签入版本
This commit is contained in:
@@ -1,179 +1,128 @@
|
||||
# 6.7 文章管理
|
||||
|
||||
:::tip 概述
|
||||
文章管理模块支持富文本内容编辑、封面图上传、分类关联。继承 BaseTenantLogic 实现租户隔离,并配置了数据权限(按创建人过滤)。
|
||||
文章管理模块支持封面图上传、图集多图、分类关联。`ArticleService` 继承 `BaseTenantService` 实现租户隔离,`use HasFileProcessing` 启用文件字段自动处理,并配置了数据权限(按创建人过滤)。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseTenantLogic(租户隔离) |
|
||||
| 文件字段 | cover(封面图),保存到 article 目录 |
|
||||
| 富文本字段 | content(文章内容),自动处理临时文件 |
|
||||
| 枚举映射 | status → article_status 字典(0=正常 1=下架) |
|
||||
| 数据权限 | dataScopeUserField='create_user',按创建人过滤 |
|
||||
| 模糊搜索 | title、author |
|
||||
| 精确匹配 | category_id、status |
|
||||
| 基类 | `BaseTenantService`(租户隔离 + 数据权限由基类提供) |
|
||||
| Trait 组合 | `HasCrudOperations, HasFileProcessing, HasQueryScopes, HasSerializeMaps` |
|
||||
| 文件字段 | `cover`(封面图,保存到 article 目录) |
|
||||
| 多文件字段 | `images`(图集,逗号分隔多 URL 逐个处理) |
|
||||
| 枚举映射 | `status` → article_status 字典 |
|
||||
| 数据权限 | `dataScopeUserField='create_user'`,按创建人过滤 |
|
||||
| 模糊搜索 | `title`、`author` |
|
||||
| 精确匹配 | `category_id`、`status` |
|
||||
| 关联钩子 | `afterDetail` 补充分类名称等关联数据 |
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/article/page` | sys:article:page | 分页列表 |
|
||||
| GET | `/api/article/list` | - | 全量列表 |
|
||||
| GET | `/api/article/detail/:id` | sys:article:detail | 详情 |
|
||||
| GET | `/api/article/list` | — | 全量列表 |
|
||||
| GET | `/api/article/detail/{id}` | sys:article:detail | 详情 |
|
||||
| POST | `/api/article/add` | sys:article:add | 新增 |
|
||||
| PUT | `/api/article/update` | sys:article:update | 修改 |
|
||||
| DELETE | `/api/article/delete/:id` | sys:article:delete | 删除 |
|
||||
| DELETE | `/api/article/delete/{id}` | sys:article:delete | 删除 |
|
||||
| DELETE | `/api/article/batchDelete` | sys:article:batchDelete | 批量删除 |
|
||||
|
||||
## 核心代码
|
||||
## Service 配置(来自项目实际代码)
|
||||
|
||||
```php
|
||||
// app/logic/ArticleLogic.php
|
||||
|
||||
class ArticleLogic extends BaseTenantLogic
|
||||
class ArticleService extends BaseTenantService
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
// 文件字段处理由 HasFileProcessing 提供;HasDataScope 由基类提供
|
||||
use HasCrudOperations, HasFileProcessing, HasQueryScopes, HasSerializeMaps;
|
||||
|
||||
protected string $modelClass = Article::class;
|
||||
|
||||
/**
|
||||
* 需要自动处理的文件上传字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
// 需要自动处理的文件上传字段
|
||||
protected array $fileFields = ['cover'];
|
||||
|
||||
/**
|
||||
* 文件保存的子目录
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
// 多文件字段(逗号分隔的多个 URL,逐个迁移/补全)
|
||||
protected array $multiFileFields = ['images'];
|
||||
|
||||
// 富文本字段:保存时迁移媒体并替换为 [IMG_URL] 占位,读取时替换回域名
|
||||
protected array $contentFields = ['content'];
|
||||
|
||||
// 文件保存的子目录
|
||||
protected string $fileSaveDir = 'article';
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
// LIKE 模糊匹配字段
|
||||
protected array $pageLikeFields = ['title', 'author'];
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
// 精确匹配字段
|
||||
protected array $pageEqFields = ['category_id', 'status'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
// 默认排序(租户内按排序值)
|
||||
protected array $pageOrderBy = [
|
||||
['field' => 'tenant_id', 'type' => 'asc'],
|
||||
['field' => 'sort', 'type' => 'asc'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
// 字典翻译
|
||||
protected array $serializeMaps = [
|
||||
'status' => 'article_status',
|
||||
];
|
||||
|
||||
/**
|
||||
* 富文本字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $contentFields = ['content'];
|
||||
|
||||
/**
|
||||
* 数据权限:归属字段名(用于按创建人过滤)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
// 数据权限:归属字段(按创建人过滤)
|
||||
protected string $dataScopeUserField = 'create_user';
|
||||
|
||||
// 详情后处理:补充分类名称等关联数据
|
||||
protected function afterDetail(int $id, array &$data): void
|
||||
{
|
||||
// 补充关联显示字段...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Controller 代码
|
||||
## Controller 代码
|
||||
|
||||
```php
|
||||
// app/controller/ArticleController.php
|
||||
// app/Http/Controllers/Api/ArticleController.php(形态示例)
|
||||
|
||||
class ArticleController extends BaseController
|
||||
#[OperationLog('文章管理-查询分页', OperationLog::TYPE_QUERY)]
|
||||
#[Permission('sys:article:page', '文章分页')]
|
||||
public function page(): JsonResponse
|
||||
{
|
||||
protected ArticleLogic $logic;
|
||||
return $this->_page($this->articleService);
|
||||
}
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->logic = new ArticleLogic();
|
||||
}
|
||||
#[OperationLog('文章管理-新增', OperationLog::TYPE_ADD, '新增文章:{title}')]
|
||||
#[Permission('sys:article:add', '添加文章')]
|
||||
public function add(ArticleRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_add($this->articleService, $request->validated());
|
||||
}
|
||||
|
||||
#[Log('文章管理-查询分页记录', Log::TYPE_QUERY)]
|
||||
#[Permission('sys:article:page', '文章分页')]
|
||||
public function page(): Json
|
||||
{
|
||||
return parent::_index($this->logic);
|
||||
}
|
||||
|
||||
#[Log('文章管理-新增记录', Log::TYPE_ADD, '新增文章:{title}')]
|
||||
#[Permission('sys:article:add', '添加文章')]
|
||||
public function add(): Json
|
||||
{
|
||||
try {
|
||||
$id = $this->logic->add($this->getJsonBody());
|
||||
return $this->success(['id' => $id], '添加成功');
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[Log('文章管理-修改记录', Log::TYPE_UPDATE, '修改文章ID:{id}')]
|
||||
#[Permission('sys:article:update', '修改文章')]
|
||||
public function update(): Json
|
||||
{
|
||||
$data = $this->getJsonBody();
|
||||
$id = $data['id'] ?? 0;
|
||||
if (empty($id)) return $this->fail('缺少id参数');
|
||||
try {
|
||||
$this->logic->update($id, $data);
|
||||
return $this->success(null, '修改成功');
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[Log('文章管理-删除记录', Log::TYPE_DELETE, '删除文章ID:{id}')]
|
||||
#[Permission('sys:article:delete', '删除文章')]
|
||||
public function delete(int $id): Json
|
||||
{
|
||||
return parent::_remove($this->logic, $id);
|
||||
}
|
||||
|
||||
#[Log('文章管理-批量删除', Log::TYPE_DELETE)]
|
||||
#[Permission('sys:article:delete', '删除文章')]
|
||||
public function batchDelete(): Json
|
||||
{
|
||||
$ids = $this->getJsonBody()['ids'] ?? [];
|
||||
return parent::_batchRemove($this->logic, $ids);
|
||||
}
|
||||
#[OperationLog('文章管理-删除', OperationLog::TYPE_DELETE, '删除文章ID:{id}')]
|
||||
#[Permission('sys:article:delete', '删除文章')]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
return $this->_delete($this->articleService, $id);
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 数据权限
|
||||
文章模块配置了 `dataScopeUserField = 'create_user'`,普通用户只能看到自己创建的文章。管理员(data_scope=1)可看到全部文章。
|
||||
文章模块配置了 `dataScopeUserField = 'create_user'`(服务由 `BaseTenantService` 提供 `HasDataScope`)——数据范围解析为 3(仅本人)时,普通用户只能看到自己创建的文章;当前角色一律解析为 1(全部数据),见 [5.9 数据权限](/zh/core/data-scope.html)。
|
||||
:::
|
||||
|
||||
:::warning 富文本处理
|
||||
content 字段配置为富文本字段后,新增/修改时会自动处理内容中的临时文件:解析 `<img>` 标签的 src,临时文件迁移到正式目录,替换为 `[IMG_URL]` 占位符。读取时自动替换为实际域名。
|
||||
:::
|
||||
:::warning 文件字段处理依赖 Trait
|
||||
`fileFields` / `multiFileFields` 声明后必须 `use HasFileProcessing`(本模块已启用),否则 `BaseService` 中的占位实现会直接跳过处理——封面图将停留在临时路径。详见 [4.7 横切机制](/zh/architecture/cross-cutting.html)。
|
||||
:::
|
||||
|
||||
## 文件处理流程
|
||||
|
||||
```text
|
||||
封面 cover(单文件字段):
|
||||
传入临时 URL → processFileFieldsOnSave 迁移到 article/ 存相对路径
|
||||
读取 → processFileFieldsOnRead 补全 FILE_DOMAIN_URL 域名
|
||||
|
||||
图集 images(多文件字段):
|
||||
传入 "url1,url2" → 逐个迁移、去域名 → 逗号拼接入库
|
||||
读取 → 逐个补全域名 → 逗号拼接返回
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user