Просмотр исходного кода

feat(article): 新增文章模块后台控制器和辅助类

- 新增 ArticleCateController 和 ArticleController后台控制器
- 新增 FilterHelper、FormHelper、GridHelper 和 ShowHelper 辅助类
- 实现文章分类和文章管理的列表、详情和表单页面- 添加文章模块的开发计划和相关文档
Your Name 8 месяцев назад
Родитель
Сommit
b865da4364
24 измененных файлов с 2639 добавлено и 15 удалено
  1. 204 0
      app/Module/Article/AdminControllers/ArticleCateController.php
  2. 79 15
      app/Module/Article/AdminControllers/ArticleController.php
  3. 15 0
      app/Module/Article/AdminControllers/Helper/FilterHelper.php
  4. 124 0
      app/Module/Article/AdminControllers/Helper/FilterHelperTrait.php
  5. 15 0
      app/Module/Article/AdminControllers/Helper/FormHelper.php
  6. 151 0
      app/Module/Article/AdminControllers/Helper/FormHelperTrait.php
  7. 15 0
      app/Module/Article/AdminControllers/Helper/GridHelper.php
  8. 155 0
      app/Module/Article/AdminControllers/Helper/GridHelperTrait.php
  9. 15 0
      app/Module/Article/AdminControllers/Helper/ShowHelper.php
  10. 143 0
      app/Module/Article/AdminControllers/Helper/ShowHelperTrait.php
  11. 90 0
      app/Module/Article/Docs/DEV.md
  12. 36 0
      app/Module/Article/Docs/README.md
  13. 207 0
      app/Module/Article/Docs/事件系统.md
  14. 165 0
      app/Module/Article/Docs/数据库设计.md
  15. 118 0
      app/Module/Article/Docs/设计概述.md
  16. 34 0
      app/Module/Article/Events/ArticleCreatedEvent.php
  17. 43 0
      app/Module/Article/Events/ArticleUpdatedEvent.php
  18. 34 0
      app/Module/Article/Events/ArticleViewedEvent.php
  19. 139 0
      app/Module/Article/Listeners/ArticleEventListener.php
  20. 151 0
      app/Module/Article/Logics/ArticleCategoryLogic.php
  21. 187 0
      app/Module/Article/Logics/ArticleLogic.php
  22. 82 0
      app/Module/Article/Providers/ArticleServiceProvider.php
  23. 164 0
      app/Module/Article/Services/ArticleCategoryService.php
  24. 273 0
      app/Module/Article/Services/ArticleService.php

+ 204 - 0
app/Module/Article/AdminControllers/ArticleCateController.php

@@ -0,0 +1,204 @@
+<?php
+
+namespace App\Module\Article\AdminControllers;
+
+use App\Module\Article\AdminControllers\Helper\FilterHelper;
+use App\Module\Article\AdminControllers\Helper\FormHelper;
+use App\Module\Article\AdminControllers\Helper\GridHelper;
+use App\Module\Article\AdminControllers\Helper\ShowHelper;
+use App\Module\Article\Enums\STATUS;
+use App\Module\Article\Models\ArticleCate;
+use Dcat\Admin\Form;
+use Dcat\Admin\Grid;
+use Dcat\Admin\Layout\Content;
+use Dcat\Admin\Layout\Row;
+use Dcat\Admin\Show;
+use Dcat\Admin\Tree;
+use Spatie\RouteAttributes\Attributes\Resource;
+use UCore\DcatAdmin\AdminController;
+
+/**
+ * 文章分类控制器
+ */
+#[Resource('article-cates', names: 'dcat.admin.article-cates')]
+class ArticleCateController extends AdminController
+{
+    /**
+     * 页面标题
+     *
+     * @var string
+     */
+    protected $title = '文章分类管理';
+
+    /**
+     * 首页
+     *
+     * @param Content $content
+     * @return Content
+     */
+    public function index(Content $content)
+    {
+        return $content
+            ->title($this->title)
+            ->description('管理文章分类')
+            ->body(function (Row $row) {
+                $row->column(6, $this->treeView());
+                $row->column(6, $this->grid());
+            });
+    }
+
+    /**
+     * 树形视图
+     *
+     * @return Tree
+     */
+    protected function treeView()
+    {
+        return new Tree(new ArticleCate(), function (Tree $tree) {
+            $tree->branch(function ($branch) {
+                $status = $branch['status'] == STATUS::SHOW->value ? '显示' : '隐藏';
+                $icon = $branch['img'] ? "<img src='{$branch['img']}' width='30' height='30'>" : '';
+
+                return "{$icon} {$branch['title']} ({$branch['unid']}) <span class='text-muted'>[{$status}]</span>";
+            });
+        });
+    }
+
+    /**
+     * 列表页
+     *
+     * @return Grid
+     */
+    protected function grid()
+    {
+        return Grid::make(new ArticleCate(), function (Grid $grid) {
+            $helper = new GridHelper($grid, $this);
+
+            $helper->columnId();
+            $grid->column('title', '分类名称');
+            $grid->column('unid', '标识');
+            $grid->column('img', '图标')->image('', 50, 50);
+            $helper->columnStatus();
+            $grid->column('desc1', '描述')->limit(30);
+            $grid->column('pid', '父级分类')->display(function ($pid) {
+                if (!$pid) return '顶级';
+                $parent = ArticleCate::find($pid);
+                return $parent ? $parent->title : '';
+            });
+            $helper->columnCreatedAt();
+            $helper->columnUpdatedAt();
+
+            // 筛选
+            $grid->filter(function (Grid\Filter $filter) {
+                $helper = new FilterHelper($filter, $this);
+                $helper->equalId();
+                $filter->like('title', '分类名称');
+                $filter->like('unid', '标识');
+                $helper->equalStatus();
+                $filter->equal('pid', '父级分类')->select(function () {
+                    $options = [0 => '顶级'];
+                    $categories = ArticleCate::all();
+                    foreach ($categories as $category) {
+                        $options[$category->id] = $category->title;
+                    }
+                    return $options;
+                });
+            });
+
+            // 禁用批量删除
+            $grid->disableBatchDelete();
+
+            // 行操作
+            $grid->actions(function (Grid\Displayers\Actions $actions) {
+                // 如果不可删除,禁用删除按钮
+                if (!$actions->row->can_delete) {
+                    $actions->disableDelete();
+                }
+            });
+        });
+    }
+
+    /**
+     * 详情页
+     *
+     * @param mixed $id
+     * @return Show
+     */
+    protected function detail($id)
+    {
+        return Show::make($id, new ArticleCate(), function (Show $show) {
+            $helper = new ShowHelper($show, $this);
+
+            $helper->fieldId();
+            $show->field('title', '分类名称');
+            $show->field('unid', '标识');
+            $show->field('img', '图标')->image();
+            $show->field('status', '状态')->using(STATUS::getValueDescription());
+            $show->field('desc1', '描述');
+            $show->field('pid', '父级分类')->as(function ($pid) {
+                if (!$pid) return '顶级';
+                $parent = ArticleCate::find($pid);
+                return $parent ? $parent->title : '';
+            });
+            $show->field('can_delete', '可删除')->as(function ($value) {
+                return $value ? '是' : '否';
+            });
+            $helper->fieldCreatedAt();
+            $helper->fieldUpdatedAt();
+        });
+    }
+
+    /**
+     * 表单
+     *
+     * @return Form
+     */
+    protected function form()
+    {
+        return Form::make(new ArticleCate(), function (Form $form) {
+            $helper = new FormHelper($form, $this);
+
+            $form->display('id', 'ID');
+            $form->text('title', '分类名称')->required()->rules('required|max:200');
+            $form->text('unid', '标识')->required()->rules('required|max:100');
+            $form->image('img', '图标')
+                ->uniqueName()
+                ->autoUpload();
+            $helper->selectStatus();
+            $form->textarea('desc1', '描述')->rows(3)->rules('max:200');
+
+            // 父级分类选择,排除自己及其子分类
+            $form->select('pid', '父级分类')
+                ->options(function () use ($form) {
+                    $categories = ArticleCate::all();
+                    $options = [0 => '顶级'];
+
+                    // 当前分类ID
+                    $currentId = $form->model()->id;
+
+                    foreach ($categories as $category) {
+                        // 排除自己及其子分类
+                        if ($currentId && ($category->id == $currentId || $category->pid == $currentId)) {
+                            continue;
+                        }
+                        $options[$category->id] = $category->title;
+                    }
+
+                    return $options;
+                })
+                ->default(0);
+
+            // 系统分类不可删除
+            if ($form->isEditing()) {
+                $form->switch('can_delete', '可删除')
+                    ->default(1)
+                    ->help('系统分类不可删除');
+            } else {
+                $form->hidden('can_delete')->value(1);
+            }
+
+            $form->display('created_at', '创建时间');
+            $form->display('updated_at', '更新时间');
+        });
+    }
+}

+ 79 - 15
app/Module/Article/AdminControllers/ArticleController.php

@@ -2,6 +2,10 @@
 
 namespace App\Module\Article\AdminControllers;
 
+use App\Module\Article\AdminControllers\Helper\FilterHelper;
+use App\Module\Article\AdminControllers\Helper\FormHelper;
+use App\Module\Article\AdminControllers\Helper\GridHelper;
+use App\Module\Article\AdminControllers\Helper\ShowHelper;
 use App\Module\Article\Enums\STATUS;
 use App\Module\Article\Models\Article;
 use App\Module\Article\Models\ArticleCate;
@@ -9,13 +13,23 @@ use App\Module\Article\Repositories\ArticleRepositories;
 use Dcat\Admin\Admin;
 use Dcat\Admin\Form;
 use Dcat\Admin\Grid;
+use Dcat\Admin\Show;
 use Dcat\Admin\Http\Controllers\AdminController;
 use Spatie\RouteAttributes\Attributes\Resource;
 
-
+/**
+ * 文章管理控制器
+ */
 #[Resource('article')]
 class ArticleController extends AdminController
 {
+    /**
+     * 页面标题
+     *
+     * @var string
+     */
+    protected $title = '文章管理';
+
     /**
      * 创建 Repository 实例
      *
@@ -27,24 +41,39 @@ class ArticleController extends AdminController
     }
 
     /**
+     * 列表页
+     *
      * @return Grid
      */
     protected function grid()
     {
         return Grid::make($this->repository(), function (Grid $grid) {
-            $grid->column('id')->sortable();
-            $grid->column('title')->limit(30);
-            $grid->column('category_id', '分类')->using(ArticleCate::getCate());
-            $grid->column('status', '状态')->using(STATUS::getValueDescription());
-            $grid->column('created_at')->sortable();
-            $grid->column('updated_at')->sortable();
+            $helper = new GridHelper($grid, $this);
+
+            $helper->columnId();
+            $helper->columnTitle();
+            $helper->columnCategory();
+            $helper->columnStatus();
+            $helper->columnViewsCount();
+            $helper->columnIsTop();
+            $helper->columnIsRecommend();
+            $helper->columnSortOrder();
+            $helper->columnCreatedAt();
+            $helper->columnUpdatedAt();
 
+            // 筛选
             $grid->filter(function (Grid\Filter $filter) {
-                $filter->like('title', '标题');
-                $filter->equal('category_id', '分类')->select(ArticleCate::getCate());
-                $filter->equal('status', '状态')->select(STATUS::getValueDescription());
+                $helper = new FilterHelper($filter, $this);
+                $helper->equalId();
+                $helper->likeTitle();
+                $helper->equalCategory();
+                $helper->equalStatus();
+                $helper->equalIsTop();
+                $helper->equalIsRecommend();
+                $helper->betweenCreatedAt();
             });
 
+            // 行操作
             $grid->actions(function (Grid\Displayers\Actions $actions) {
                 $actions->disableView();
             });
@@ -52,16 +81,51 @@ class ArticleController extends AdminController
     }
 
     /**
+     * 详情页
+     *
+     * @param mixed $id
+     * @return Show
+     */
+    protected function detail($id)
+    {
+        return Show::make($id, new Article(), function (Show $show) {
+            $helper = new ShowHelper($show, $this);
+
+            $helper->fieldId();
+            $helper->fieldTitle();
+            $helper->fieldDescription();
+            $helper->fieldCategory();
+            $helper->fieldStatus();
+            $helper->fieldViewsCount();
+            $helper->fieldIsTop();
+            $helper->fieldIsRecommend();
+            $helper->fieldSortOrder();
+            $helper->fieldContent();
+            $helper->fieldCreatedBy();
+            $helper->fieldCreatedAt();
+            $helper->fieldUpdatedAt();
+        });
+    }
+
+    /**
+     * 表单
+     *
      * @return Form
      */
     protected function form()
     {
         return Form::make(Article::class, function (Form $form) {
-            $form->text('title', '标题')->required();
-            $form->radio('category_id', '分类')->options(ArticleCate::getCate())->required();
-            $form->radio('status')->options(STATUS::getValueDescription());
-            $form->editor('content', '内容')->required();
-            $form->hidden('created_by')->value(Admin::user()->getOriginal('id'));
+            $helper = new FormHelper($form, $this);
+
+            $helper->textTitle();
+            $helper->textareaDescription();
+            $helper->selectCategory();
+            $helper->selectStatus();
+            $helper->numberSortOrder();
+            $helper->switchIsTop();
+            $helper->switchIsRecommend();
+            $helper->editorContent();
+            $helper->hiddenCreatedBy();
         });
     }
 }

+ 15 - 0
app/Module/Article/AdminControllers/Helper/FilterHelper.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Module\Article\AdminControllers\Helper;
+
+use UCore\DcatAdmin\FilterHelper as BaseFilterHelper;
+
+/**
+ * 筛选器辅助类
+ *
+ * 提供文章模块后台控制器的筛选器构建功能
+ */
+class FilterHelper extends BaseFilterHelper
+{
+    use FilterHelperTrait;
+}

+ 124 - 0
app/Module/Article/AdminControllers/Helper/FilterHelperTrait.php

@@ -0,0 +1,124 @@
+<?php
+
+namespace App\Module\Article\AdminControllers\Helper;
+
+use App\Module\Article\Enums\STATUS;
+use App\Module\Article\Models\ArticleCate;
+use Dcat\Admin\Grid\Filter;
+use Dcat\Admin\Grid\Filter\Presenter\Presenter;
+
+/**
+ * 筛选器辅助特性
+ *
+ * 提供文章模块后台控制器的筛选器构建功能的具体实现
+ */
+trait FilterHelperTrait
+{
+    /**
+     * 添加文章ID筛选
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Filter\AbstractFilter
+     */
+    public function equalId(string $field = 'id', string $label = 'ID'): Filter\AbstractFilter
+    {
+        return $this->filter->equal($field, $label);
+    }
+
+    /**
+     * 添加文章标题筛选
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Filter\AbstractFilter
+     */
+    public function likeTitle(string $field = 'title', string $label = '标题'): Filter\AbstractFilter
+    {
+        return $this->filter->like($field, $label);
+    }
+
+    /**
+     * 添加文章分类筛选
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Presenter
+     */
+    public function equalCategory(string $field = 'category_id', string $label = '分类'): Presenter
+    {
+        return $this->filter->equal($field, $label)->select(ArticleCate::getCate());
+    }
+
+    /**
+     * 添加文章状态筛选
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Presenter
+     */
+    public function equalStatus(string $field = 'status', string $label = '状态'): Presenter
+    {
+        return $this->filter->equal($field, $label)->select(STATUS::getValueDescription());
+    }
+
+    /**
+     * 添加文章是否置顶筛选
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Presenter
+     */
+    public function equalIsTop(string $field = 'is_top', string $label = '是否置顶'): Presenter
+    {
+        return $this->filter->equal($field, $label)->select([0 => '否', 1 => '是']);
+    }
+
+    /**
+     * 添加文章是否推荐筛选
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Presenter
+     */
+    public function equalIsRecommend(string $field = 'is_recommend', string $label = '是否推荐'): Presenter
+    {
+        return $this->filter->equal($field, $label)->select([0 => '否', 1 => '是']);
+    }
+
+    /**
+     * 添加文章创建时间筛选
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Filter\AbstractFilter
+     */
+    public function betweenCreatedAt(string $field = 'created_at', string $label = '创建时间'): Filter\AbstractFilter
+    {
+        return $this->filter->between($field, $label)->datetime();
+    }
+
+    /**
+     * 添加文章更新时间筛选
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Filter\AbstractFilter
+     */
+    public function betweenUpdatedAt(string $field = 'updated_at', string $label = '更新时间'): Filter\AbstractFilter
+    {
+        return $this->filter->between($field, $label)->datetime();
+    }
+
+    /**
+     * 添加文章创建者筛选
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Filter\AbstractFilter
+     */
+    public function equalCreatedBy(string $field = 'created_by', string $label = '创建者'): Filter\AbstractFilter
+    {
+        return $this->filter->equal($field, $label);
+    }
+}

+ 15 - 0
app/Module/Article/AdminControllers/Helper/FormHelper.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Module\Article\AdminControllers\Helper;
+
+use UCore\DcatAdmin\FormHelper as BaseFormHelper;
+
+/**
+ * 表单辅助类
+ *
+ * 提供文章模块后台控制器的表单构建功能
+ */
+class FormHelper extends BaseFormHelper
+{
+    use FormHelperTrait;
+}

+ 151 - 0
app/Module/Article/AdminControllers/Helper/FormHelperTrait.php

@@ -0,0 +1,151 @@
+<?php
+
+namespace App\Module\Article\AdminControllers\Helper;
+
+use App\Module\Article\Enums\STATUS;
+use App\Module\Article\Models\ArticleCate;
+use Dcat\Admin\Form;
+use Dcat\Admin\Form\Field;
+
+/**
+ * 表单辅助特性
+ *
+ * 提供文章模块后台控制器的表单构建功能的具体实现
+ */
+trait FormHelperTrait
+{
+    /**
+     * 添加文章标题输入
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Field\Text
+     */
+    public function textTitle(string $field = 'title', string $label = '标题'): Field\Text
+    {
+        return $this->form->text($field, $label)
+            ->required()
+            ->rules('required|max:200');
+    }
+
+    /**
+     * 添加文章描述输入
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Field\Textarea
+     */
+    public function textareaDescription(string $field = 'description', string $label = '简述'): Field\Textarea
+    {
+        return $this->form->textarea($field, $label)
+            ->rows(3)
+            ->rules('max:200');
+    }
+
+    /**
+     * 添加文章分类选择
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Field\Select|Field\Radio
+     */
+    public function selectCategory(string $field = 'category_id', string $label = '分类', bool $useRadio = true)
+    {
+        $options = ArticleCate::getCate();
+
+        if ($useRadio) {
+            return $this->form->radio($field, $label)
+                ->options($options)
+                ->required();
+        }
+
+        return $this->form->select($field, $label)
+            ->options($options)
+            ->required();
+    }
+
+    /**
+     * 添加文章状态选择
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Field\Select|Field\Radio
+     */
+    public function selectStatus(string $field = 'status', string $label = '状态', bool $useRadio = true)
+    {
+        $options = STATUS::getValueDescription();
+
+        if ($useRadio) {
+            return $this->form->radio($field, $label)
+                ->options($options)
+                ->default(STATUS::SHOW->value);
+        }
+
+        return $this->form->select($field, $label)
+            ->options($options)
+            ->default(STATUS::SHOW->value);
+    }
+
+    /**
+     * 添加文章内容编辑器
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Field\Editor
+     */
+    public function editorContent(string $field = 'content', string $label = '内容'): Field\Editor
+    {
+        return $this->form->editor($field, $label)->required();
+    }
+
+    /**
+     * 添加文章排序输入
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Field\Number
+     */
+    public function numberSortOrder(string $field = 'sort_order', string $label = '排序'): Field\Number
+    {
+        return $this->form->number($field, $label)
+            ->default(0)
+            ->help('数字越小越靠前');
+    }
+
+    /**
+     * 添加文章是否置顶开关
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Field\Switch
+     */
+    public function switchIsTop(string $field = 'is_top', string $label = '是否置顶'): Field\Switch
+    {
+        return $this->form->switch($field, $label)->default(0);
+    }
+
+    /**
+     * 添加文章是否推荐开关
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Field\Switch
+     */
+    public function switchIsRecommend(string $field = 'is_recommend', string $label = '是否推荐'): Field\Switch
+    {
+        return $this->form->switch($field, $label)->default(0);
+    }
+
+    /**
+     * 添加文章创建者隐藏输入
+     *
+     * @param string $field 字段名
+     * @param int|null $userId 用户ID
+     * @return Field\Hidden
+     */
+    public function hiddenCreatedBy(string $field = 'created_by', ?int $userId = null): Field\Hidden
+    {
+        $value = $userId ?? \Dcat\Admin\Admin::user()->getOriginal('id');
+        return $this->form->hidden($field)->value($value);
+    }
+}

+ 15 - 0
app/Module/Article/AdminControllers/Helper/GridHelper.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Module\Article\AdminControllers\Helper;
+
+use UCore\DcatAdmin\GridHelper as BaseGridHelper;
+
+/**
+ * 列表页辅助类
+ *
+ * 提供文章模块后台控制器的列表页构建功能
+ */
+class GridHelper extends BaseGridHelper
+{
+    use GridHelperTrait;
+}

+ 155 - 0
app/Module/Article/AdminControllers/Helper/GridHelperTrait.php

@@ -0,0 +1,155 @@
+<?php
+
+namespace App\Module\Article\AdminControllers\Helper;
+
+use App\Module\Article\Enums\STATUS;
+use App\Module\Article\Models\ArticleCate;
+use Dcat\Admin\Grid;
+use Dcat\Admin\Grid\Column;
+
+/**
+ * 列表页辅助特性
+ *
+ * 提供文章模块后台控制器的列表页构建功能的具体实现
+ */
+trait GridHelperTrait
+{
+    /**
+     * 添加文章标题列
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @param int $limit 限制长度
+     * @return Column
+     */
+    public function columnTitle(string $field = 'title', string $label = '标题', int $limit = 30): Column
+    {
+        return $this->grid->column($field, $label)->limit($limit);
+    }
+
+    /**
+     * 添加文章描述列
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @param int $limit 限制长度
+     * @return Column
+     */
+    public function columnDescription(string $field = 'description', string $label = '简述', int $limit = 50): Column
+    {
+        return $this->grid->column($field, $label)->limit($limit);
+    }
+
+    /**
+     * 添加文章分类列
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Column
+     */
+    public function columnCategory(string $field = 'category_id', string $label = '分类'): Column
+    {
+        return $this->grid->column($field, $label)->using(ArticleCate::getCate());
+    }
+
+    /**
+     * 添加文章状态列
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Column
+     */
+    public function columnStatus(string $field = 'status', string $label = '状态'): Column
+    {
+        return $this->grid->column($field, $label)
+            ->using(STATUS::getValueDescription())
+            ->label([
+                STATUS::DISABLE->value => 'danger',
+                STATUS::SHOW->value => 'success',
+            ]);
+    }
+
+    /**
+     * 添加文章排序列
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Column
+     */
+    public function columnSortOrder(string $field = 'sort_order', string $label = '排序'): Column
+    {
+        return $this->grid->column($field, $label)->sortable();
+    }
+
+    /**
+     * 添加文章是否置顶列
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Column
+     */
+    public function columnIsTop(string $field = 'is_top', string $label = '置顶'): Column
+    {
+        return $this->grid->column($field, $label)->switch();
+    }
+
+    /**
+     * 添加文章是否推荐列
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Column
+     */
+    public function columnIsRecommend(string $field = 'is_recommend', string $label = '推荐'): Column
+    {
+        return $this->grid->column($field, $label)->switch();
+    }
+
+    /**
+     * 添加文章浏览量列
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Column
+     */
+    public function columnViewsCount(string $field = 'views_count', string $label = '浏览量'): Column
+    {
+        return $this->grid->column($field, $label)->sortable();
+    }
+
+    /**
+     * 添加文章创建者列
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Column
+     */
+    public function columnCreatedBy(string $field = 'created_by', string $label = '创建者'): Column
+    {
+        return $this->grid->column($field, $label);
+    }
+
+    /**
+     * 添加文章创建时间列
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Column
+     */
+    public function columnCreatedAt(string $field = 'created_at', string $label = '创建时间'): Column
+    {
+        return $this->grid->column($field, $label)->sortable();
+    }
+
+    /**
+     * 添加文章更新时间列
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Column
+     */
+    public function columnUpdatedAt(string $field = 'updated_at', string $label = '更新时间'): Column
+    {
+        return $this->grid->column($field, $label)->sortable();
+    }
+}

+ 15 - 0
app/Module/Article/AdminControllers/Helper/ShowHelper.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Module\Article\AdminControllers\Helper;
+
+use UCore\DcatAdmin\ShowHelper as BaseShowHelper;
+
+/**
+ * 详情页辅助类
+ *
+ * 提供文章模块后台控制器的详情页构建功能
+ */
+class ShowHelper extends BaseShowHelper
+{
+    use ShowHelperTrait;
+}

+ 143 - 0
app/Module/Article/AdminControllers/Helper/ShowHelperTrait.php

@@ -0,0 +1,143 @@
+<?php
+
+namespace App\Module\Article\AdminControllers\Helper;
+
+use App\Module\Article\Enums\STATUS;
+use App\Module\Article\Models\ArticleCate;
+use Dcat\Admin\Show;
+
+/**
+ * 详情页辅助特性
+ *
+ * 提供文章模块后台控制器的详情页构建功能的具体实现
+ */
+trait ShowHelperTrait
+{
+    /**
+     * 显示文章标题
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Show\Field
+     */
+    public function fieldTitle(string $field = 'title', string $label = '标题'): Show\Field
+    {
+        return $this->show->field($field, $label);
+    }
+
+    /**
+     * 显示文章描述
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Show\Field
+     */
+    public function fieldDescription(string $field = 'description', string $label = '简述'): Show\Field
+    {
+        return $this->show->field($field, $label);
+    }
+
+    /**
+     * 显示文章分类
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Show\Field
+     */
+    public function fieldCategory(string $field = 'category_id', string $label = '分类'): Show\Field
+    {
+        return $this->show->field($field, $label)->as(function ($value) {
+            return ArticleCate::getCate()[$value] ?? '未知分类';
+        });
+    }
+
+    /**
+     * 显示文章状态
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Show\Field
+     */
+    public function fieldStatus(string $field = 'status', string $label = '状态'): Show\Field
+    {
+        return $this->show->field($field, $label)->as(function ($value) {
+            return STATUS::getValueDescription()[$value] ?? '未知状态';
+        });
+    }
+
+    /**
+     * 显示文章内容
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Show\Field
+     */
+    public function fieldContent(string $field = 'content', string $label = '内容'): Show\Field
+    {
+        return $this->show->field($field, $label)->unescape();
+    }
+
+    /**
+     * 显示文章排序
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Show\Field
+     */
+    public function fieldSortOrder(string $field = 'sort_order', string $label = '排序'): Show\Field
+    {
+        return $this->show->field($field, $label);
+    }
+
+    /**
+     * 显示文章是否置顶
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Show\Field
+     */
+    public function fieldIsTop(string $field = 'is_top', string $label = '是否置顶'): Show\Field
+    {
+        return $this->show->field($field, $label)->as(function ($value) {
+            return $value ? '是' : '否';
+        });
+    }
+
+    /**
+     * 显示文章是否推荐
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Show\Field
+     */
+    public function fieldIsRecommend(string $field = 'is_recommend', string $label = '是否推荐'): Show\Field
+    {
+        return $this->show->field($field, $label)->as(function ($value) {
+            return $value ? '是' : '否';
+        });
+    }
+
+    /**
+     * 显示文章浏览量
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Show\Field
+     */
+    public function fieldViewsCount(string $field = 'views_count', string $label = '浏览量'): Show\Field
+    {
+        return $this->show->field($field, $label);
+    }
+
+    /**
+     * 显示文章创建者
+     *
+     * @param string $field 字段名
+     * @param string $label 标签名
+     * @return Show\Field
+     */
+    public function fieldCreatedBy(string $field = 'created_by', string $label = '创建者'): Show\Field
+    {
+        return $this->show->field($field, $label);
+    }
+}

+ 90 - 0
app/Module/Article/Docs/DEV.md

@@ -0,0 +1,90 @@
+# 文章模块开发计划
+
+## 1. 概述
+
+本文档描述了文章模块的开发计划和任务分解,包括各个组件的实现顺序和依赖关系。
+
+## 2. 开发环境准备
+
+- [x] 创建模块目录结构
+- [x] 配置开发环境
+- [x] 设置数据库连接
+
+## 3. 数据库设计与实现
+
+- [x] 设计文章表结构
+- [x] 设计分类表结构
+- [x] 创建数据库迁移文件
+- [x] 执行数据库迁移
+
+## 4. 实现模型层
+
+- [x] Article模型
+- [x] ArticleCate模型
+
+## 5. 实现数据仓库
+
+- [x] ArticleRepositories
+- [ ] ArticleCategoryRepository
+
+## 6. 实现逻辑层
+
+- [x] ArticleLogic
+- [x] ArticleCategoryLogic
+
+## 7. 实现服务层
+
+- [x] ArticleService
+- [x] ArticleCategoryService
+
+## 8. 实现事件和监听器
+
+- [x] ArticleCreatedEvent
+- [x] ArticleUpdatedEvent
+- [x] ArticleViewedEvent
+- [x] ArticleEventListener
+
+## 9. 实现后台控制器
+
+- [x] ArticleController
+- [x] ArticleCateController
+- [x] 控制器Helper类
+
+## 10. 实现服务提供者
+
+- [x] ArticleServiceProvider
+
+## 11. 文档编写
+
+- [x] README.md
+- [x] 设计概述.md
+- [x] 数据库设计.md
+- [x] 事件系统.md
+- [ ] 缓存策略.md
+- [ ] 与其他模块集成.md
+
+## 12. 测试
+
+- [ ] 单元测试
+  - [ ] 模型测试
+  - [ ] 服务测试
+  - [ ] 逻辑测试
+- [ ] 功能测试
+  - [ ] 控制器测试
+  - [ ] API测试
+- [ ] 集成测试
+  - [ ] 与其他模块的集成测试
+
+## 13. 部署与上线
+
+- [ ] 准备部署脚本
+- [ ] 执行数据库迁移
+- [ ] 配置生产环境
+- [ ] 上线测试
+
+## 14. 后续优化
+
+- [ ] 性能优化
+- [ ] 缓存策略优化
+- [ ] 代码重构
+- [ ] 功能扩展

+ 36 - 0
app/Module/Article/Docs/README.md

@@ -0,0 +1,36 @@
+# 文章模块文档索引
+
+## 概述
+
+文章模块是系统中的内容管理功能,用于管理各类文章的创建、编辑、发布和展示。本目录包含了文章模块的详细设计文档。
+
+## 文档目录
+
+### 1. 基础设计文档
+
+- [设计概述](设计概述.md) - 模块的整体设计思路和架构
+- [数据库设计](数据库设计.md) - 详细的数据库表结构和关系设计
+- [DEV](DEV.md) - 模块的开发计划和进度
+
+### 2. 功能领域文档
+
+- [文章管理系统](文章管理系统.md) - 文章的创建、编辑、发布和管理
+- [分类管理系统](分类管理系统.md) - 文章分类的管理和维护
+- [内容展示系统](内容展示系统.md) - 文章内容的前端展示和浏览
+
+### 3. 开发与实现文档
+
+- [开发指南](开发指南.md) - 模块开发的快速入门指南
+- [事件系统](事件系统.md) - 模块的事件系统设计与实现
+- [缓存策略](缓存策略.md) - 文章模块的缓存策略设计与实现
+
+### 4. 模块集成文档
+
+- [与其他模块集成](与其他模块集成.md) - 文章模块与其他模块的集成方案
+
+## 文档更新记录
+
+| 日期 | 版本 | 更新内容 | 更新人 |
+| ---- | ---- | -------- | ------ |
+| 2023-06-01 | v1.0 | 初始版本 | 系统管理员 |
+| 2023-07-15 | v1.1 | 添加缓存策略文档 | 系统管理员 |

+ 207 - 0
app/Module/Article/Docs/事件系统.md

@@ -0,0 +1,207 @@
+# 文章模块事件系统
+
+## 1. 概述
+
+文章模块的事件系统用于在文章生命周期的关键节点触发事件,允许其他模块或系统组件响应这些事件并执行相应的操作。这种设计实现了模块间的松耦合,提高了系统的可扩展性和可维护性。
+
+## 2. 事件定义
+
+### 2.1 ArticleCreatedEvent
+
+当新文章创建时触发。
+
+**事件类:** `App\Module\Article\Events\ArticleCreatedEvent`
+
+**事件数据:**
+```php
+/**
+ * 文章模型
+ * @var Article
+ */
+public Article $article;
+```
+
+**触发时机:**
+- 通过后台管理界面创建文章
+- 通过API接口创建文章
+- 通过导入功能批量创建文章
+
+### 2.2 ArticleUpdatedEvent
+
+当文章信息更新时触发。
+
+**事件类:** `App\Module\Article\Events\ArticleUpdatedEvent`
+
+**事件数据:**
+```php
+/**
+ * 文章模型
+ * @var Article
+ */
+public Article $article;
+
+/**
+ * 原始数据
+ * @var array
+ */
+public array $originalData;
+```
+
+**触发时机:**
+- 通过后台管理界面更新文章
+- 通过API接口更新文章
+- 批量更新文章状态或属性
+
+### 2.3 ArticleViewedEvent
+
+当文章被浏览时触发。
+
+**事件类:** `App\Module\Article\Events\ArticleViewedEvent`
+
+**事件数据:**
+```php
+/**
+ * 文章模型
+ * @var Article
+ */
+public Article $article;
+```
+
+**触发时机:**
+- 用户访问文章详情页
+- 通过API接口获取文章详情
+
+## 3. 事件监听器
+
+### 3.1 ArticleEventListener
+
+监听文章相关事件,执行相应的操作。
+
+**监听器类:** `App\Module\Article\Listeners\ArticleEventListener`
+
+**职责:**
+- 处理文章创建事件,记录日志,清除缓存
+- 处理文章更新事件,记录日志,清除缓存
+- 处理文章浏览事件,记录日志,更新统计数据
+
+## 4. 事件注册
+
+文章模块通过服务提供者注册事件和监听器:
+
+```php
+/**
+ * 文章模块服务提供者
+ */
+class ArticleServiceProvider extends ServiceProvider
+{
+    /**
+     * 事件到监听器的映射
+     *
+     * @var array
+     */
+    protected $listen = [
+        ArticleCreatedEvent::class => [
+            ArticleEventListener::class . '@handleArticleCreated',
+        ],
+        ArticleUpdatedEvent::class => [
+            ArticleEventListener::class . '@handleArticleUpdated',
+        ],
+        ArticleViewedEvent::class => [
+            ArticleEventListener::class . '@handleArticleViewed',
+        ],
+    ];
+
+    /**
+     * 需要注册的订阅者
+     *
+     * @var array
+     */
+    protected $subscribe = [
+        ArticleEventListener::class,
+    ];
+
+    // ...
+}
+```
+
+## 5. 使用示例
+
+### 5.1 触发事件
+
+```php
+// 创建文章后触发事件
+$article = Article::create($data);
+event(new ArticleCreatedEvent($article));
+
+// 更新文章后触发事件
+$originalData = $article->getOriginal();
+$article->update($data);
+event(new ArticleUpdatedEvent($article, $originalData));
+
+// 浏览文章时触发事件
+$article = Article::find($id);
+event(new ArticleViewedEvent($article));
+```
+
+### 5.2 监听事件
+
+```php
+// 在其他模块中监听文章事件
+protected $listen = [
+    ArticleCreatedEvent::class => [
+        SendArticleNotificationListener::class,
+    ],
+    ArticleViewedEvent::class => [
+        UpdateArticleStatisticsListener::class,
+    ],
+];
+```
+
+## 6. 与其他模块的集成
+
+文章模块的事件系统可以与其他模块集成,实现跨模块功能:
+
+### 6.1 与通知模块集成
+
+通知模块可以监听文章事件,向相关用户发送通知:
+
+```php
+// 在通知模块的服务提供者中
+protected $listen = [
+    ArticleCreatedEvent::class => [
+        SendNewArticleNotificationListener::class,
+    ],
+    ArticleUpdatedEvent::class => [
+        SendArticleUpdateNotificationListener::class,
+    ],
+];
+```
+
+### 6.2 与统计模块集成
+
+统计模块可以监听文章事件,更新相关统计数据:
+
+```php
+// 在统计模块的服务提供者中
+protected $listen = [
+    ArticleViewedEvent::class => [
+        UpdateArticleViewStatisticsListener::class,
+    ],
+];
+```
+
+### 6.3 与搜索模块集成
+
+搜索模块可以监听文章事件,更新搜索索引:
+
+```php
+// 在搜索模块的服务提供者中
+protected $listen = [
+    ArticleCreatedEvent::class => [
+        IndexNewArticleListener::class,
+    ],
+    ArticleUpdatedEvent::class => [
+        UpdateArticleIndexListener::class,
+    ],
+];
+```

+ 165 - 0
app/Module/Article/Docs/数据库设计.md

@@ -0,0 +1,165 @@
+# 文章模块数据库设计
+
+## 1. 数据库表概览
+
+文章模块包含以下数据库表:
+
+| 表名 | 描述 |
+| ---- | ---- |
+| `articles` | 存储文章基本信息和内容 |
+| `article_cates` | 存储文章分类信息 |
+
+## 2. 表结构详细设计
+
+### 2.1 articles 表
+
+存储文章的基本信息和内容。
+
+#### 字段定义
+
+| 字段名 | 类型 | 允许空 | 默认值 | 描述 |
+| ------ | ---- | ------ | ------ | ---- |
+| `id` | bigint unsigned | 否 | 自增 | 主键ID |
+| `title` | varchar(200) | 否 | 无 | 文章标题 |
+| `description` | varchar(200) | 是 | NULL | 文章简述 |
+| `category_id` | int | 否 | 无 | 分类ID |
+| `created_by` | int | 否 | 无 | 创建人ID |
+| `views_count` | int | 否 | 0 | 浏览量 |
+| `status` | varchar(5) | 否 | 无 | 状态 |
+| `sort_order` | int | 否 | 0 | 排序 |
+| `is_top` | tinyint(1) | 否 | 0 | 是否置顶 |
+| `is_recommend` | tinyint(1) | 否 | 0 | 是否推荐 |
+| `content` | text | 否 | 无 | 文章内容 |
+| `created_at` | timestamp | 是 | NULL | 创建时间 |
+| `updated_at` | timestamp | 是 | NULL | 更新时间 |
+| `deleted_at` | timestamp | 是 | NULL | 删除时间 |
+
+#### 索引
+
+| 索引名 | 类型 | 字段 |
+| ------ | ---- | ---- |
+| `PRIMARY` | 主键 | `id` |
+| `idx_category_id` | 普通索引 | `category_id` |
+| `idx_status` | 普通索引 | `status` |
+| `idx_is_top` | 普通索引 | `is_top` |
+| `idx_is_recommend` | 普通索引 | `is_recommend` |
+| `idx_created_at` | 普通索引 | `created_at` |
+
+### 2.2 article_cates 表
+
+存储文章分类信息。
+
+#### 字段定义
+
+| 字段名 | 类型 | 允许空 | 默认值 | 描述 |
+| ------ | ---- | ------ | ------ | ---- |
+| `id` | bigint unsigned | 否 | 自增 | 主键ID |
+| `pid` | int | 否 | 0 | 父级分类ID,0表示顶级分类 |
+| `title` | varchar(200) | 否 | 无 | 分类名称 |
+| `unid` | varchar(100) | 否 | 无 | 分类标识 |
+| `img` | varchar(255) | 是 | NULL | 分类图标 |
+| `status` | varchar(5) | 否 | 无 | 状态 |
+| `desc1` | varchar(200) | 是 | NULL | 分类描述 |
+| `can_delete` | tinyint(1) | 否 | 1 | 是否可以删除的分类 |
+| `created_at` | timestamp | 是 | NULL | 创建时间 |
+| `updated_at` | timestamp | 是 | NULL | 更新时间 |
+| `deleted_at` | timestamp | 是 | NULL | 删除时间 |
+
+#### 索引
+
+| 索引名 | 类型 | 字段 |
+| ------ | ---- | ---- |
+| `PRIMARY` | 主键 | `id` |
+| `idx_pid` | 普通索引 | `pid` |
+| `idx_unid` | 唯一索引 | `unid` |
+| `idx_status` | 普通索引 | `status` |
+
+## 3. 表关系
+
+### 3.1 articles 与 article_cates 的关系
+
+- 关系类型:多对一
+- 关系描述:一个分类可以包含多篇文章,一篇文章只能属于一个分类
+- 关联字段:`articles.category_id` 关联 `article_cates.id`
+
+### 3.2 article_cates 的自关联
+
+- 关系类型:自关联(一对多)
+- 关系描述:一个分类可以有多个子分类,一个子分类只能有一个父分类
+- 关联字段:`article_cates.pid` 关联 `article_cates.id`
+
+## 4. 数据库脚本
+
+### 4.1 创建 articles 表
+
+```sql
+DROP TABLE IF EXISTS `kku_articles`;
+CREATE TABLE `kku_articles` (
+  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+  `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标题',
+  `description` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '简述',
+  `category_id` int NOT NULL COMMENT '分类ID',
+  `created_by` int NOT NULL COMMENT '创建人ID',
+  `views_count` int NOT NULL DEFAULT '0' COMMENT '观看数',
+  `status` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '状态',
+  `sort_order` int NOT NULL DEFAULT '0' COMMENT '排序',
+  `is_top` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否置顶',
+  `is_recommend` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否推荐',
+  `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '内容',
+  `created_at` timestamp NULL DEFAULT NULL,
+  `updated_at` timestamp NULL DEFAULT NULL,
+  `deleted_at` timestamp NULL DEFAULT NULL COMMENT '删除时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_category_id` (`category_id`),
+  KEY `idx_status` (`status`),
+  KEY `idx_is_top` (`is_top`),
+  KEY `idx_is_recommend` (`is_recommend`),
+  KEY `idx_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+```
+
+### 4.2 创建 article_cates 表
+
+```sql
+DROP TABLE IF EXISTS `kku_article_cates`;
+CREATE TABLE `kku_article_cates` (
+  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+  `pid` int NOT NULL DEFAULT '0' COMMENT '父级分类',
+  `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标题',
+  `unid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标识',
+  `img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '首图',
+  `status` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '状态',
+  `desc1` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '分类描述',
+  `can_delete` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否可以删除的分类',
+  `created_at` timestamp NULL DEFAULT NULL,
+  `updated_at` timestamp NULL DEFAULT NULL,
+  `deleted_at` timestamp NULL DEFAULT NULL COMMENT '删除时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_pid` (`pid`),
+  UNIQUE KEY `idx_unid` (`unid`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+```
+
+## 5. 数据字典
+
+### 5.1 状态枚举
+
+| 值 | 描述 |
+| -- | ---- |
+| `show` | 显示 |
+| `hide` | 隐藏 |
+
+### 5.2 是否置顶/推荐枚举
+
+| 值 | 描述 |
+| -- | ---- |
+| `0` | 否 |
+| `1` | 是 |
+
+### 5.3 是否可删除枚举
+
+| 值 | 描述 |
+| -- | ---- |
+| `0` | 不可删除(系统分类) |
+| `1` | 可删除 |

+ 118 - 0
app/Module/Article/Docs/设计概述.md

@@ -0,0 +1,118 @@
+# 文章模块设计概述
+
+## 1. 模块简介
+
+文章模块是系统中的内容管理功能,用于管理各类文章的创建、编辑、发布和展示。该模块提供了完整的文章生命周期管理,包括文章分类、文章内容、文章状态等功能。
+
+## 2. 核心功能
+
+- **文章管理**:创建、编辑、发布、删除文章
+- **分类管理**:创建、编辑、删除文章分类,支持多级分类
+- **内容展示**:前端展示文章内容,支持列表和详情页
+- **状态控制**:控制文章的显示/隐藏状态
+- **特殊标记**:支持置顶、推荐等特殊标记
+- **浏览统计**:记录文章浏览量
+
+## 3. 模块架构
+
+### 3.1 目录结构
+
+文章模块采用标准的模块化目录结构,便于代码组织和维护:
+
+```
+app/Module/Article/
+├── AdminControllers/        # 后台管理控制器
+│   ├── Helper/              # 控制器辅助类
+│   └── Actions/             # 控制器动作类
+├── Databases/               # 数据库相关文件
+│   └── GenerateSql/         # 数据库创建脚本
+├── Docs/                    # 模块文档
+├── Enums/                   # 枚举类型定义
+├── Events/                  # 事件类
+├── Listeners/               # 事件监听器
+├── Logics/                  # 业务逻辑类
+├── Models/                  # 数据模型
+├── Providers/               # 服务提供者
+├── Repositories/            # 数据仓库
+└── Services/                # 服务类
+```
+
+### 3.2 核心组件
+
+#### 3.2.1 模型层 (Models)
+
+文章模块的核心数据模型包括:
+
+- **Article**:文章模型,存储文章的基本信息和内容
+- **ArticleCate**:文章分类模型,定义文章的分类结构
+
+#### 3.2.2 仓库层 (Repositories)
+
+仓库层封装数据访问逻辑,提供统一的数据操作接口:
+
+- **ArticleRepositories**:文章仓库,负责文章数据的CRUD操作
+- **ArticleCategoryRepository**:文章分类仓库,管理文章分类数据
+
+#### 3.2.3 逻辑层 (Logics)
+
+逻辑层包含核心业务逻辑,仅供模块内部使用:
+
+- **ArticleLogic**:文章核心逻辑,处理文章的创建、更新、删除等
+- **ArticleCategoryLogic**:分类逻辑,处理分类的创建、更新、删除等
+
+#### 3.2.4 服务层 (Services)
+
+服务层对外提供功能接口,是模块与外部交互的主要方式:
+
+- **ArticleService**:文章服务,提供文章的查询、创建、更新等功能
+- **ArticleCategoryService**:分类服务,提供分类的查询、创建、更新等功能
+
+#### 3.2.5 控制器层 (AdminControllers)
+
+控制器层处理HTTP请求,提供后台管理界面:
+
+- **ArticleController**:文章管理控制器,提供文章的CRUD操作界面
+- **ArticleCateController**:文章分类管理控制器,管理文章分类
+
+#### 3.2.6 事件系统 (Events & Listeners)
+
+文章模块定义了以下核心事件:
+
+- **ArticleCreatedEvent**:文章创建事件
+- **ArticleUpdatedEvent**:文章更新事件
+- **ArticleViewedEvent**:文章浏览事件
+
+## 4. 设计原则
+
+文章模块的设计遵循以下原则:
+
+### 4.1 单一职责原则
+每个类只负责一个功能领域,如文章管理、分类管理等。
+
+### 4.2 开闭原则
+模块设计为可扩展的,新功能可以通过扩展而非修改现有代码来实现。
+
+### 4.3 依赖倒置原则
+高层模块不应该依赖低层模块,两者都应该依赖抽象。
+
+### 4.4 接口隔离原则
+不强制客户端依赖于它们不使用的接口。
+
+### 4.5 最小知识原则
+一个对象应该对其他对象有最少的了解。
+
+## 5. 与其他模块的交互
+
+文章模块主要与以下模块交互:
+
+- **用户模块**:获取文章创建者信息
+- **文件模块**:处理文章中的图片上传和存储
+- **通知模块**:发送文章相关的通知
+
+## 6. 扩展点
+
+文章模块提供以下扩展点:
+
+- **事件系统**:其他模块可以监听文章模块的事件
+- **服务接口**:其他模块可以调用文章模块的服务接口
+- **自定义字段**:可以通过扩展添加自定义字段

+ 34 - 0
app/Module/Article/Events/ArticleCreatedEvent.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Module\Article\Events;
+
+use App\Module\Article\Models\Article;
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+
+/**
+ * 文章创建事件
+ */
+class ArticleCreatedEvent
+{
+    use Dispatchable, InteractsWithSockets, SerializesModels;
+
+    /**
+     * 文章模型
+     *
+     * @var Article
+     */
+    public Article $article;
+
+    /**
+     * 创建一个新的事件实例
+     *
+     * @param Article $article
+     * @return void
+     */
+    public function __construct(Article $article)
+    {
+        $this->article = $article;
+    }
+}

+ 43 - 0
app/Module/Article/Events/ArticleUpdatedEvent.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Module\Article\Events;
+
+use App\Module\Article\Models\Article;
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+
+/**
+ * 文章更新事件
+ */
+class ArticleUpdatedEvent
+{
+    use Dispatchable, InteractsWithSockets, SerializesModels;
+
+    /**
+     * 文章模型
+     *
+     * @var Article
+     */
+    public Article $article;
+
+    /**
+     * 原始数据
+     *
+     * @var array
+     */
+    public array $originalData;
+
+    /**
+     * 创建一个新的事件实例
+     *
+     * @param Article $article
+     * @param array $originalData
+     * @return void
+     */
+    public function __construct(Article $article, array $originalData)
+    {
+        $this->article = $article;
+        $this->originalData = $originalData;
+    }
+}

+ 34 - 0
app/Module/Article/Events/ArticleViewedEvent.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Module\Article\Events;
+
+use App\Module\Article\Models\Article;
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+
+/**
+ * 文章浏览事件
+ */
+class ArticleViewedEvent
+{
+    use Dispatchable, InteractsWithSockets, SerializesModels;
+
+    /**
+     * 文章模型
+     *
+     * @var Article
+     */
+    public Article $article;
+
+    /**
+     * 创建一个新的事件实例
+     *
+     * @param Article $article
+     * @return void
+     */
+    public function __construct(Article $article)
+    {
+        $this->article = $article;
+    }
+}

+ 139 - 0
app/Module/Article/Listeners/ArticleEventListener.php

@@ -0,0 +1,139 @@
+<?php
+
+namespace App\Module\Article\Listeners;
+
+use App\Module\Article\Events\ArticleCreatedEvent;
+use App\Module\Article\Events\ArticleUpdatedEvent;
+use App\Module\Article\Events\ArticleViewedEvent;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 文章事件监听器
+ */
+class ArticleEventListener
+{
+    /**
+     * 处理文章创建事件
+     *
+     * @param ArticleCreatedEvent $event
+     * @return void
+     */
+    public function handleArticleCreated(ArticleCreatedEvent $event): void
+    {
+        try {
+            // 记录日志
+            Log::info('文章创建', [
+                'article_id' => $event->article->id,
+                'title' => $event->article->title,
+                'category_id' => $event->article->category_id,
+                'created_by' => $event->article->created_by,
+            ]);
+
+            // 清除相关缓存
+            $this->clearArticleCache();
+
+            // 这里可以添加其他处理逻辑,如发送通知等
+        } catch (\Exception $e) {
+            Log::error('处理文章创建事件失败', [
+                'error' => $e->getMessage(),
+                'article_id' => $event->article->id,
+            ]);
+        }
+    }
+
+    /**
+     * 处理文章更新事件
+     *
+     * @param ArticleUpdatedEvent $event
+     * @return void
+     */
+    public function handleArticleUpdated(ArticleUpdatedEvent $event): void
+    {
+        try {
+            // 记录日志
+            Log::info('文章更新', [
+                'article_id' => $event->article->id,
+                'title' => $event->article->title,
+                'category_id' => $event->article->category_id,
+                'original_data' => $event->originalData,
+            ]);
+
+            // 清除相关缓存
+            $this->clearArticleCache();
+
+            // 这里可以添加其他处理逻辑,如发送通知等
+        } catch (\Exception $e) {
+            Log::error('处理文章更新事件失败', [
+                'error' => $e->getMessage(),
+                'article_id' => $event->article->id,
+            ]);
+        }
+    }
+
+    /**
+     * 处理文章浏览事件
+     *
+     * @param ArticleViewedEvent $event
+     * @return void
+     */
+    public function handleArticleViewed(ArticleViewedEvent $event): void
+    {
+        try {
+            // 记录日志
+            Log::info('文章浏览', [
+                'article_id' => $event->article->id,
+                'title' => $event->article->title,
+                'views_count' => $event->article->views_count,
+            ]);
+
+            // 这里可以添加其他处理逻辑,如统计分析等
+        } catch (\Exception $e) {
+            Log::error('处理文章浏览事件失败', [
+                'error' => $e->getMessage(),
+                'article_id' => $event->article->id,
+            ]);
+        }
+    }
+
+    /**
+     * 清除文章相关缓存
+     *
+     * @return void
+     */
+    protected function clearArticleCache(): void
+    {
+        Cache::forget('article_recommend_5');
+        Cache::forget('article_top_5');
+
+        // 清除分类文章缓存
+        $categoryIds = Cache::get('article_category_ids', []);
+        foreach ($categoryIds as $categoryId) {
+            Cache::forget('article_category_' . $categoryId . '_10');
+        }
+    }
+
+    /**
+     * 注册监听器
+     *
+     * @param \Illuminate\Events\Dispatcher $events
+     * @return void
+     */
+    public function subscribe($events): void
+    {
+        $events->listen(
+            ArticleCreatedEvent::class,
+            [ArticleEventListener::class, 'handleArticleCreated']
+        );
+
+        $events->listen(
+            ArticleUpdatedEvent::class,
+            [ArticleEventListener::class, 'handleArticleUpdated']
+        );
+
+        $events->listen(
+            ArticleViewedEvent::class,
+            [ArticleEventListener::class, 'handleArticleViewed']
+        );
+    }
+}

+ 151 - 0
app/Module/Article/Logics/ArticleCategoryLogic.php

@@ -0,0 +1,151 @@
+<?php
+
+namespace App\Module\Article\Logics;
+
+use App\Module\Article\Models\Article;
+use App\Module\Article\Models\ArticleCate;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 文章分类逻辑类
+ *
+ * 处理文章分类相关的业务逻辑
+ */
+class ArticleCategoryLogic
+{
+    /**
+     * 创建分类
+     *
+     * @param array $data 分类数据
+     * @return ArticleCate
+     */
+    public static function createCategory(array $data): ArticleCate
+    {
+        try {
+            return DB::transaction(function () use ($data) {
+                return ArticleCate::create($data);
+            });
+        } catch (\Exception $e) {
+            Log::error('创建文章分类失败', [
+                'error' => $e->getMessage(),
+                'data' => $data
+            ]);
+            throw $e;
+        }
+    }
+
+    /**
+     * 更新分类
+     *
+     * @param int $id 分类ID
+     * @param array $data 分类数据
+     * @return bool
+     */
+    public static function updateCategory(int $id, array $data): bool
+    {
+        try {
+            return DB::transaction(function () use ($id, $data) {
+                $category = ArticleCate::find($id);
+
+                if (!$category) {
+                    return false;
+                }
+
+                return $category->update($data);
+            });
+        } catch (\Exception $e) {
+            Log::error('更新文章分类失败', [
+                'error' => $e->getMessage(),
+                'id' => $id,
+                'data' => $data
+            ]);
+            return false;
+        }
+    }
+
+    /**
+     * 删除分类
+     *
+     * @param int $id 分类ID
+     * @return bool
+     */
+    public static function deleteCategory(int $id): bool
+    {
+        try {
+            return DB::transaction(function () use ($id) {
+                $category = ArticleCate::find($id);
+
+                if (!$category) {
+                    return false;
+                }
+
+                // 检查是否可以删除
+                if (!$category->can_delete) {
+                    throw new \Exception('该分类不允许删除');
+                }
+
+                // 检查是否有子分类
+                $childCount = ArticleCate::where('pid', $id)->count();
+                if ($childCount > 0) {
+                    throw new \Exception('该分类下有子分类,不能删除');
+                }
+
+                // 检查是否有关联文章
+                $articleCount = Article::where('category_id', $id)->count();
+                if ($articleCount > 0) {
+                    throw new \Exception('该分类下有文章,不能删除');
+                }
+
+                return $category->delete();
+            });
+        } catch (\Exception $e) {
+            Log::error('删除文章分类失败', [
+                'error' => $e->getMessage(),
+                'id' => $id
+            ]);
+            throw $e;
+        }
+    }
+
+    /**
+     * 批量更新分类状态
+     *
+     * @param array $ids 分类ID数组
+     * @param string $status 状态
+     * @return bool
+     */
+    public static function batchUpdateStatus(array $ids, string $status): bool
+    {
+        try {
+            return DB::transaction(function () use ($ids, $status) {
+                return ArticleCate::whereIn('id', $ids)->update(['status' => $status]);
+            });
+        } catch (\Exception $e) {
+            Log::error('批量更新文章分类状态失败', [
+                'error' => $e->getMessage(),
+                'ids' => $ids,
+                'status' => $status
+            ]);
+            return false;
+        }
+    }
+
+    /**
+     * 检查分类标识是否唯一
+     *
+     * @param string $unid 分类标识
+     * @param int|null $excludeId 排除的分类ID
+     * @return bool
+     */
+    public static function isUnidUnique(string $unid, ?int $excludeId = null): bool
+    {
+        $query = ArticleCate::where('unid', $unid);
+
+        if ($excludeId) {
+            $query->where('id', '!=', $excludeId);
+        }
+
+        return $query->count() === 0;
+    }
+}

+ 187 - 0
app/Module/Article/Logics/ArticleLogic.php

@@ -0,0 +1,187 @@
+<?php
+
+namespace App\Module\Article\Logics;
+
+use App\Module\Article\Models\Article;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 文章逻辑类
+ *
+ * 处理文章相关的业务逻辑
+ */
+class ArticleLogic
+{
+    /**
+     * 创建文章
+     *
+     * @param array $data 文章数据
+     * @return Article
+     */
+    public static function createArticle(array $data): Article
+    {
+        try {
+            return DB::transaction(function () use ($data) {
+                return Article::create($data);
+            });
+        } catch (\Exception $e) {
+            Log::error('创建文章失败', [
+                'error' => $e->getMessage(),
+                'data' => $data
+            ]);
+            throw $e;
+        }
+    }
+
+    /**
+     * 更新文章
+     *
+     * @param int $id 文章ID
+     * @param array $data 文章数据
+     * @return bool
+     */
+    public static function updateArticle(int $id, array $data): bool
+    {
+        try {
+            return DB::transaction(function () use ($id, $data) {
+                $article = Article::find($id);
+
+                if (!$article) {
+                    return false;
+                }
+
+                return $article->update($data);
+            });
+        } catch (\Exception $e) {
+            Log::error('更新文章失败', [
+                'error' => $e->getMessage(),
+                'id' => $id,
+                'data' => $data
+            ]);
+            return false;
+        }
+    }
+
+    /**
+     * 删除文章
+     *
+     * @param int $id 文章ID
+     * @return bool
+     */
+    public static function deleteArticle(int $id): bool
+    {
+        try {
+            return DB::transaction(function () use ($id) {
+                $article = Article::find($id);
+
+                if (!$article) {
+                    return false;
+                }
+
+                return $article->delete();
+            });
+        } catch (\Exception $e) {
+            Log::error('删除文章失败', [
+                'error' => $e->getMessage(),
+                'id' => $id
+            ]);
+            return false;
+        }
+    }
+
+    /**
+     * 增加文章浏览量
+     *
+     * @param int $id 文章ID
+     * @return bool
+     */
+    public static function incrementViewCount(int $id): bool
+    {
+        try {
+            $article = Article::find($id);
+
+            if (!$article) {
+                return false;
+            }
+
+            $article->increment('views_count');
+            return true;
+        } catch (\Exception $e) {
+            Log::error('增加文章浏览量失败', [
+                'error' => $e->getMessage(),
+                'id' => $id
+            ]);
+            return false;
+        }
+    }
+
+    /**
+     * 批量更新文章状态
+     *
+     * @param array $ids 文章ID数组
+     * @param string $status 状态
+     * @return bool
+     */
+    public static function batchUpdateStatus(array $ids, string $status): bool
+    {
+        try {
+            return DB::transaction(function () use ($ids, $status) {
+                return Article::whereIn('id', $ids)->update(['status' => $status]);
+            });
+        } catch (\Exception $e) {
+            Log::error('批量更新文章状态失败', [
+                'error' => $e->getMessage(),
+                'ids' => $ids,
+                'status' => $status
+            ]);
+            return false;
+        }
+    }
+
+    /**
+     * 批量更新文章置顶状态
+     *
+     * @param array $ids 文章ID数组
+     * @param bool $isTop 是否置顶
+     * @return bool
+     */
+    public static function batchUpdateTopStatus(array $ids, bool $isTop): bool
+    {
+        try {
+            return DB::transaction(function () use ($ids, $isTop) {
+                return Article::whereIn('id', $ids)->update(['is_top' => $isTop]);
+            });
+        } catch (\Exception $e) {
+            Log::error('批量更新文章置顶状态失败', [
+                'error' => $e->getMessage(),
+                'ids' => $ids,
+                'is_top' => $isTop
+            ]);
+            return false;
+        }
+    }
+
+    /**
+     * 批量更新文章推荐状态
+     *
+     * @param array $ids 文章ID数组
+     * @param bool $isRecommend 是否推荐
+     * @return bool
+     */
+    public static function batchUpdateRecommendStatus(array $ids, bool $isRecommend): bool
+    {
+        try {
+            return DB::transaction(function () use ($ids, $isRecommend) {
+                return Article::whereIn('id', $ids)->update(['is_recommend' => $isRecommend]);
+            });
+        } catch (\Exception $e) {
+            Log::error('批量更新文章推荐状态失败', [
+                'error' => $e->getMessage(),
+                'ids' => $ids,
+                'is_recommend' => $isRecommend
+            ]);
+            return false;
+        }
+    }
+}

+ 82 - 0
app/Module/Article/Providers/ArticleServiceProvider.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace App\Module\Article\Providers;
+
+use App\Module\Article\Events\ArticleCreatedEvent;
+use App\Module\Article\Events\ArticleUpdatedEvent;
+use App\Module\Article\Events\ArticleViewedEvent;
+use App\Module\Article\Listeners\ArticleEventListener;
+use Illuminate\Support\ServiceProvider;
+
+/**
+ * 文章模块服务提供者
+ */
+class ArticleServiceProvider extends ServiceProvider
+{
+    /**
+     * 事件到监听器的映射
+     *
+     * @var array
+     */
+    protected $listen = [
+        ArticleCreatedEvent::class => [
+            ArticleEventListener::class . '@handleArticleCreated',
+        ],
+        ArticleUpdatedEvent::class => [
+            ArticleEventListener::class . '@handleArticleUpdated',
+        ],
+        ArticleViewedEvent::class => [
+            ArticleEventListener::class . '@handleArticleViewed',
+        ],
+    ];
+
+    /**
+     * 需要注册的订阅者
+     *
+     * @var array
+     */
+    protected $subscribe = [
+        ArticleEventListener::class,
+    ];
+
+    /**
+     * 注册服务
+     *
+     * @return void
+     */
+    public function register(): void
+    {
+        // 注册服务...
+    }
+
+    /**
+     * 启动服务
+     *
+     * @return void
+     */
+    public function boot(): void
+    {
+        // 注册事件监听器
+        $this->registerEvents();
+    }
+
+    /**
+     * 注册事件和监听器
+     *
+     * @return void
+     */
+    protected function registerEvents(): void
+    {
+        $events = $this->app['events'];
+
+        foreach ($this->listen as $event => $listeners) {
+            foreach ($listeners as $listener) {
+                $events->listen($event, $listener);
+            }
+        }
+
+        foreach ($this->subscribe as $subscriber) {
+            $events->subscribe($subscriber);
+        }
+    }
+}

+ 164 - 0
app/Module/Article/Services/ArticleCategoryService.php

@@ -0,0 +1,164 @@
+<?php
+
+namespace App\Module\Article\Services;
+
+use App\Module\Article\Enums\STATUS;
+use App\Module\Article\Logics\ArticleCategoryLogic;
+use App\Module\Article\Models\ArticleCate;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Cache;
+
+/**
+ * 文章分类服务类
+ *
+ * 提供文章分类相关的服务方法,供其他模块调用
+ */
+class ArticleCategoryService
+{
+    /**
+     * 获取所有分类
+     *
+     * @param bool $onlyActive 是否只获取激活状态的分类
+     * @return Collection
+     */
+    public static function getAllCategories(bool $onlyActive = true): Collection
+    {
+        $cacheKey = 'article_categories_' . ($onlyActive ? 'active' : 'all');
+
+        return Cache::remember($cacheKey, 3600, function () use ($onlyActive) {
+            $query = ArticleCate::query();
+
+            if ($onlyActive) {
+                $query->where('status', STATUS::SHOW->value);
+            }
+
+            return $query->orderBy('sort_order', 'asc')->get();
+        });
+    }
+
+    /**
+     * 获取分类树
+     *
+     * @param bool $onlyActive 是否只获取激活状态的分类
+     * @return array
+     */
+    public static function getCategoryTree(bool $onlyActive = true): array
+    {
+        $cacheKey = 'article_category_tree_' . ($onlyActive ? 'active' : 'all');
+
+        return Cache::remember($cacheKey, 3600, function () use ($onlyActive) {
+            $categories = self::getAllCategories($onlyActive);
+            return self::buildCategoryTree($categories);
+        });
+    }
+
+    /**
+     * 构建分类树
+     *
+     * @param Collection $categories 分类集合
+     * @param int $parentId 父级ID
+     * @return array
+     */
+    protected static function buildCategoryTree(Collection $categories, int $parentId = 0): array
+    {
+        $tree = [];
+
+        foreach ($categories as $category) {
+            if ($category->pid == $parentId) {
+                $children = self::buildCategoryTree($categories, $category->id);
+
+                if ($children) {
+                    $category->children = $children;
+                }
+
+                $tree[] = $category;
+            }
+        }
+
+        return $tree;
+    }
+
+    /**
+     * 获取分类详情
+     *
+     * @param int $id 分类ID
+     * @return ArticleCate|null
+     */
+    public static function getCategoryDetail(int $id): ?ArticleCate
+    {
+        return ArticleCate::find($id);
+    }
+
+    /**
+     * 根据标识获取分类
+     *
+     * @param string $unid 分类标识
+     * @return ArticleCate|null
+     */
+    public static function getCategoryByUnid(string $unid): ?ArticleCate
+    {
+        return ArticleCate::where('unid', $unid)->first();
+    }
+
+    /**
+     * 创建分类
+     *
+     * @param array $data 分类数据
+     * @return ArticleCate
+     */
+    public static function createCategory(array $data): ArticleCate
+    {
+        $category = ArticleCategoryLogic::createCategory($data);
+
+        // 清除相关缓存
+        self::clearCategoryCache();
+
+        return $category;
+    }
+
+    /**
+     * 更新分类
+     *
+     * @param int $id 分类ID
+     * @param array $data 分类数据
+     * @return bool
+     */
+    public static function updateCategory(int $id, array $data): bool
+    {
+        $result = ArticleCategoryLogic::updateCategory($id, $data);
+
+        // 清除相关缓存
+        self::clearCategoryCache();
+
+        return $result;
+    }
+
+    /**
+     * 删除分类
+     *
+     * @param int $id 分类ID
+     * @return bool
+     */
+    public static function deleteCategory(int $id): bool
+    {
+        $result = ArticleCategoryLogic::deleteCategory($id);
+
+        // 清除相关缓存
+        self::clearCategoryCache();
+
+        return $result;
+    }
+
+    /**
+     * 清除分类相关缓存
+     *
+     * @return void
+     */
+    public static function clearCategoryCache(): void
+    {
+        Cache::forget('article_categories_active');
+        Cache::forget('article_categories_all');
+        Cache::forget('article_category_tree_active');
+        Cache::forget('article_category_tree_all');
+    }
+}

+ 273 - 0
app/Module/Article/Services/ArticleService.php

@@ -0,0 +1,273 @@
+<?php
+
+namespace App\Module\Article\Services;
+
+use App\Module\Article\Enums\STATUS;
+use App\Module\Article\Events\ArticleCreatedEvent;
+use App\Module\Article\Events\ArticleViewedEvent;
+use App\Module\Article\Logics\ArticleLogic;
+use App\Module\Article\Models\Article;
+use App\Module\Article\Models\ArticleCate;
+use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Cache;
+
+/**
+ * 文章服务类
+ *
+ * 提供文章相关的服务方法,供其他模块调用
+ */
+class ArticleService
+{
+    /**
+     * 获取文章列表
+     *
+     * @param array $params 查询参数
+     * @param int $page 页码
+     * @param int $perPage 每页数量
+     * @return LengthAwarePaginator
+     */
+    public static function getArticleList(array $params = [], int $page = 1, int $perPage = 10): LengthAwarePaginator
+    {
+        $query = Article::query();
+
+        // 只查询显示状态的文章
+        $query->where('status', STATUS::SHOW->value);
+
+        // 分类筛选
+        if (isset($params['category_id']) && $params['category_id']) {
+            $query->where('category_id', $params['category_id']);
+        }
+
+        // 标题搜索
+        if (isset($params['title']) && $params['title']) {
+            $query->where('title', 'like', "%{$params['title']}%");
+        }
+
+        // 是否置顶
+        if (isset($params['is_top'])) {
+            $query->where('is_top', $params['is_top']);
+        }
+
+        // 是否推荐
+        if (isset($params['is_recommend'])) {
+            $query->where('is_recommend', $params['is_recommend']);
+        }
+
+        // 排序
+        $query->orderBy('is_top', 'desc')
+              ->orderBy('sort_order', 'asc')
+              ->orderBy('created_at', 'desc');
+
+        return $query->paginate($perPage, ['*'], 'page', $page);
+    }
+
+    /**
+     * 获取文章详情
+     *
+     * @param int $id 文章ID
+     * @param bool $incrementViews 是否增加浏览量
+     * @return Article|null
+     */
+    public static function getArticleDetail(int $id, bool $incrementViews = true): ?Article
+    {
+        $article = Article::find($id);
+
+        if (!$article || $article->status != STATUS::SHOW->value) {
+            return null;
+        }
+
+        // 增加浏览量
+        if ($incrementViews) {
+            self::incrementViewCount($id);
+
+            // 触发文章浏览事件
+            event(new ArticleViewedEvent($article));
+        }
+
+        return $article;
+    }
+
+    /**
+     * 增加文章浏览量
+     *
+     * @param int $id 文章ID
+     * @return bool
+     */
+    public static function incrementViewCount(int $id): bool
+    {
+        return ArticleLogic::incrementViewCount($id);
+    }
+
+    /**
+     * 获取推荐文章列表
+     *
+     * @param int $limit 数量限制
+     * @return Collection
+     */
+    public static function getRecommendArticles(int $limit = 5): Collection
+    {
+        $cacheKey = 'article_recommend_' . $limit;
+
+        return Cache::remember($cacheKey, 3600, function () use ($limit) {
+            return Article::where('status', STATUS::SHOW->value)
+                ->where('is_recommend', 1)
+                ->orderBy('sort_order', 'asc')
+                ->orderBy('created_at', 'desc')
+                ->limit($limit)
+                ->get();
+        });
+    }
+
+    /**
+     * 获取置顶文章列表
+     *
+     * @param int $limit 数量限制
+     * @return Collection
+     */
+    public static function getTopArticles(int $limit = 5): Collection
+    {
+        $cacheKey = 'article_top_' . $limit;
+
+        return Cache::remember($cacheKey, 3600, function () use ($limit) {
+            return Article::where('status', STATUS::SHOW->value)
+                ->where('is_top', 1)
+                ->orderBy('sort_order', 'asc')
+                ->orderBy('created_at', 'desc')
+                ->limit($limit)
+                ->get();
+        });
+    }
+
+    /**
+     * 获取分类文章列表
+     *
+     * @param int $categoryId 分类ID
+     * @param int $limit 数量限制
+     * @return Collection
+     */
+    public static function getCategoryArticles(int $categoryId, int $limit = 10): Collection
+    {
+        $cacheKey = 'article_category_' . $categoryId . '_' . $limit;
+
+        return Cache::remember($cacheKey, 3600, function () use ($categoryId, $limit) {
+            return Article::where('status', STATUS::SHOW->value)
+                ->where('category_id', $categoryId)
+                ->orderBy('is_top', 'desc')
+                ->orderBy('sort_order', 'asc')
+                ->orderBy('created_at', 'desc')
+                ->limit($limit)
+                ->get();
+        });
+    }
+
+    /**
+     * 获取相关文章
+     *
+     * @param int $articleId 当前文章ID
+     * @param int $categoryId 分类ID
+     * @param int $limit 数量限制
+     * @return Collection
+     */
+    public static function getRelatedArticles(int $articleId, int $categoryId, int $limit = 5): Collection
+    {
+        return Article::where('status', STATUS::SHOW->value)
+            ->where('id', '!=', $articleId)
+            ->where('category_id', $categoryId)
+            ->orderBy('created_at', 'desc')
+            ->limit($limit)
+            ->get();
+    }
+
+    /**
+     * 获取所有分类
+     *
+     * @param bool $onlyActive 是否只获取激活状态的分类
+     * @return Collection
+     */
+    public static function getAllCategories(bool $onlyActive = true): Collection
+    {
+        $cacheKey = 'article_categories_' . ($onlyActive ? 'active' : 'all');
+
+        return Cache::remember($cacheKey, 3600, function () use ($onlyActive) {
+            $query = ArticleCate::query();
+
+            if ($onlyActive) {
+                $query->where('status', STATUS::SHOW->value);
+            }
+
+            return $query->orderBy('sort_order', 'asc')->get();
+        });
+    }
+
+    /**
+     * 创建文章
+     *
+     * @param array $data 文章数据
+     * @return Article
+     */
+    public static function createArticle(array $data): Article
+    {
+        $article = ArticleLogic::createArticle($data);
+
+        // 触发文章创建事件
+        event(new ArticleCreatedEvent($article));
+
+        // 清除相关缓存
+        self::clearArticleCache();
+
+        return $article;
+    }
+
+    /**
+     * 更新文章
+     *
+     * @param int $id 文章ID
+     * @param array $data 文章数据
+     * @return bool
+     */
+    public static function updateArticle(int $id, array $data): bool
+    {
+        $result = ArticleLogic::updateArticle($id, $data);
+
+        // 清除相关缓存
+        self::clearArticleCache();
+
+        return $result;
+    }
+
+    /**
+     * 删除文章
+     *
+     * @param int $id 文章ID
+     * @return bool
+     */
+    public static function deleteArticle(int $id): bool
+    {
+        $result = ArticleLogic::deleteArticle($id);
+
+        // 清除相关缓存
+        self::clearArticleCache();
+
+        return $result;
+    }
+
+    /**
+     * 清除文章相关缓存
+     *
+     * @return void
+     */
+    public static function clearArticleCache(): void
+    {
+        Cache::forget('article_recommend_5');
+        Cache::forget('article_top_5');
+        Cache::forget('article_categories_active');
+        Cache::forget('article_categories_all');
+
+        // 清除分类文章缓存
+        $categories = ArticleCate::all();
+        foreach ($categories as $category) {
+            Cache::forget('article_category_' . $category->id . '_10');
+        }
+    }
+}