签入版本

This commit is contained in:
2026-10-08 17:50:29 +08:00
parent 809750b47e
commit e213a9b31d
444 changed files with 39366 additions and 37533 deletions
@@ -1,305 +1,280 @@
# 3.6 控制器开发
:::tip 概述
控制器是 HTTP 请求的入口,负责参数获取、调用 Logic 层、返回 JSON 响应。通过 `#[Permission]` 和 `#[Log]` 注解实现权限校验和操作日志。
控制器是 HTTP 请求的入口,负责参数获取、调用 Service、返回统一 JSON 响应。通过 `#[Permission]` 和 `#[OperationLog]` 注解实现权限校验和操作日志,配合基类快捷方法,单个接口通常只需几行代码。
:::
## 控制器文件位置
```text
app/controller/ExampleController.php
app/Http/Controllers/Api/ExampleController.php
```
## 完整代码(来自项目实际代码)
```php
<?php
declare(strict_types=1);
namespace app\controller;
namespace App\Http\Controllers\Api;
use app\BaseController; // 控制器基类
use app\logic\ExampleLogic; // 案例演示业务逻辑
use app\attribute\Log; // 操作日志注解
use app\attribute\Permission; // 权限校验注解
use think\response\Json; // JSON 响应类型
use App\Attributes\OperationLog;
use App\Attributes\Permission;
use App\Http\Controllers\Controller;
use App\Http\Requests\ExampleRequest;
use App\Services\ExampleService;
use Illuminate\Http\JsonResponse;
/**
* 案例演示管理控制器
* 示例管理控制器
*
* 负责案例演示相关的 HTTP 接口入口,包括:
* - 分页列表、列表查询、详情查询;
* - 新增、修改、删除、批量删除;
* - 导入、导出。
* 负责示例相关的接口入口,包含分页、列表、详情、新增、修改、删除、批量删除,
* 以及数据导入、导出。
* 具体业务逻辑委托给 ExampleService,控制器仅做参数校验、
* 权限与日志标注及响应封装。
*
* 具体业务逻辑委托给 ExampleLogic 处理,
* 并通过注解统一实现日志记录与权限校验。
* 说明:
* - 所有接口通过 #[Permission] 注解声明权限标识与描述,由框架统一鉴权。
* - 所有接口通过 #[OperationLog] 注解声明操作日志(类型、模板),由框架统一记录。
* - 导入 / 导出接口的日志类型分别为 TYPE_IMPORT、TYPE_EXPORT,与常规 CRUD 区分。
* - 常规 CRUD 复用基类的
* _page / _list / _detail / _add / _update / _delete / _batchDelete 方法。
* - 导入 / 导出走 ExampleService 的 import / export 专用方法。
*/
class ExampleController extends BaseController
class ExampleController extends Controller
{
/**
* 案例演示业务逻辑层
* 构造函数
*
* @var ExampleLogic
* 注入示例业务服务,供各接口方法复用。
*
* @param ExampleService $exampleService 示例业务服务
*/
protected ExampleLogic $logic;
public function __construct(protected ExampleService $exampleService) {}
/**
* 控制器初始化
* 查询分页
*
* 在每次请求进入控制器方法前执行,用于实例化业务逻辑层。
* ThinkPHP 会自动调用此方法(在构造函数之后)。
* 对应权限:sys:example:page,操作日志类型:查询。
* 返回分页结构,适合列表页展示。
*
* @return void
* @return JsonResponse 分页数据
*/
protected function initialize(): void
#[OperationLog('示例管理-查询分页', OperationLog::TYPE_QUERY)]
#[Permission('sys:example:page', '示例分页')]
public function page(): JsonResponse
{
$this->logic = new ExampleLogic();
return $this->_page($this->exampleService);
}
// ... 以下为各接口方法
/**
* 查询列表
*
* 对应权限:sys:example:list,操作日志类型:查询。
* 返回不分页的完整列表,适合下拉选择等场景。
*
* @return JsonResponse 列表数据
*/
#[OperationLog('示例管理-查询列表', OperationLog::TYPE_QUERY)]
#[Permission('sys:example:list', '示例列表')]
public function list(): JsonResponse
{
return $this->_list($this->exampleService);
}
/**
* 查询详情
*
* 对应权限:sys:example:detail,操作日志类型:查询,
* 日志模板中包含示例ID:{id}。
*
* @param int $id 示例ID
* @return JsonResponse 示例详情
*/
#[OperationLog('示例管理-查询详情', OperationLog::TYPE_QUERY, '查询示例ID:{id}')]
#[Permission('sys:example:detail', '示例详情')]
public function detail(int $id): JsonResponse
{
return $this->_detail($this->exampleService, $id);
}
/**
* 新增
*
* 对应权限:sys:example:add,操作日志类型:新增,
* 日志模板中包含示例名称:{name}。
*
* @param ExampleRequest $request 已通过校验的示例请求
* @return JsonResponse 新增结果
*/
#[OperationLog('示例管理-新增', OperationLog::TYPE_ADD, '新增示例:{name}')]
#[Permission('sys:example:add', '添加示例')]
public function add(ExampleRequest $request): JsonResponse
{
return $this->_add($this->exampleService, $request->validated());
}
/**
* 修改
*
* 对应权限:sys:example:update,操作日志类型:修改。
* 若请求数据中缺少 id,则直接返回失败响应。
*
* @param ExampleRequest $request 已通过校验的示例请求
* @return JsonResponse 修改结果
*/
#[OperationLog('示例管理-修改', OperationLog::TYPE_UPDATE, '修改示例ID:{id}')]
#[Permission('sys:example:update', '修改示例')]
public function update(ExampleRequest $request): JsonResponse
{
$data = $request->validated();
$id = $data['id'] ?? 0;
if (empty($id)) {
return $this->fail('缺少id参数');
}
return $this->_update($this->exampleService, $id, $data);
}
/**
* 删除
*
* 对应权限:sys:example:delete,操作日志类型:删除,
* 日志模板中包含示例ID:{id}。
*
* @param int $id 示例ID
* @return JsonResponse 删除结果
*/
#[OperationLog('示例管理-删除', OperationLog::TYPE_DELETE, '删除示例ID:{id}')]
#[Permission('sys:example:delete', '删除示例')]
public function delete(int $id): JsonResponse
{
return $this->_delete($this->exampleService, $id);
}
/**
* 批量删除
*
* 对应权限:sys:example:batchDelete,操作日志类型:删除。
* 从 JSON 请求体中读取 ids 数组,未传时按空数组处理。
*
* @return JsonResponse 批量删除结果
*/
#[OperationLog('示例管理-批量删除', OperationLog::TYPE_DELETE)]
#[Permission('sys:example:batchDelete', '批量删除')]
public function batchDelete(): JsonResponse
{
$ids = $this->getJsonBody()['ids'] ?? [];
return $this->_batchDelete($this->exampleService, $ids);
}
/**
* 导出数据
*
* 对应权限:sys:example:export,操作日志类型:导出(TYPE_EXPORT)。
* 由基类 _export() 统一处理文件生成与响应封装。
*
* @return JsonResponse 导出结果(data 为文件地址)
*/
#[OperationLog('示例管理-导出数据', OperationLog::TYPE_EXPORT)]
#[Permission('sys:example:export', '导出示例')]
public function export(): JsonResponse
{
return $this->_export($this->exampleService);
}
/**
* 导入数据
*
* 对应权限:sys:example:import,操作日志类型:导入(TYPE_IMPORT)。
* 由基类 _import() 统一处理文件校验(格式/大小/MIME)与响应封装。
*
* @return JsonResponse 导入结果
*/
#[OperationLog('示例管理-导入数据', OperationLog::TYPE_IMPORT, '通过Excel导入示例数据')]
#[Permission('sys:example:import', '导入示例')]
public function import(): JsonResponse
{
return $this->_import($this->exampleService);
}
}
```
:::tip 控制器模式
每个控制器遵循固定模式:
1. `initialize()` 中实例化 Logic
2. 每个方法标注 `#[Log]` 和 `#[Permission]` 注解
3. 简单调用 `parent::_index()` / `_detail()` / `_add()` 等通用方法
4. 复杂操作(如导入导出)自行处理并 try-catch
1. **构造函数注入 Service**(Laravel 依赖注入,`protected` 简写属性即成员属性)
2. 每个方法标注 `#[OperationLog]` 和 `#[Permission]` 注解
3. 简单调用 `$this->_page()` / `_detail()` / `_add()` 等快捷方法
4. 入参由 FormRequest 类型注入完成格式校验(见 [3.4 FormRequest 验证器](/zh/guide/validator.html))
:::
```php
/**
* 案例演示分页列表
*
* GET /example/page
* 对应权限:sys:example:page
*
* 调用 BaseController::_index(),内部流程:
* 1. 合并分页参数(pageNo/pageSize)和普通参数
* 2. 调用 ExampleLogic::pageList()
* 3. 返回标准分页响应
*
* @return Json
*/
#[Log('案例演示-查询分页记录', Log::TYPE_QUERY)]
#[Permission('sys:example:page', '案例演示分页')]
public function page(): Json
{
return parent::_index($this->logic);
}
## Controller 基类快捷方法
/**
* 案例演示列表
*
* GET /example/list
* 返回全量数据(不分页),适用于下拉选择等场景。
*
* @return Json
*/
#[Log('案例演示-查询列表', Log::TYPE_QUERY)]
public function list(): Json
{
return parent::_list($this->logic);
}
`app/Http/Controllers/Controller.php` 提供的通用方法:
/**
* 案例演示详情
*
* GET /example/detail/{id}
*
* 调用 BaseController::_detail(),内部流程:
* 1. 调用 ExampleLogic::detail($id)
* 2. 查询为空返回 404
* 3. 自动补全文件字段域名、枚举显示名
* 4. 触发 afterDetail 钩子
*
* @param int $id 案例演示ID
* @return Json
*/
#[Log('案例演示-查询详情', Log::TYPE_QUERY, '查询案例演示ID:{id}')]
public function detail(int $id): Json
{
return parent::_detail($this->logic, $id);
}
| 方法 | 用途 | 内部调用 | 失败场景 |
|------|------|----------|----------|
| `_page($service)` | 分页查询 | `Service::pageList()` | - |
| `_list($service)` | 全量列表 | `Service::allList()` | - |
| `_treeList($service)` | 树形列表 | `Service::treeList()` | - |
| `_detail($service, $id)` | 详情查询 | `Service::detail($id)` | 空数据返回 `code=404` |
| `_add($service, $data)` | 新增 | `Service::add($data)` | 捕获 `BizException` → `fail(消息, code)` |
| `_update($service, $id, $data)` | 修改 | `Service::update($id, $data)` | id 缺失提示;捕获 `BizException` |
| `_delete($service, $id)` | 删除 | `Service::delete($id)` | 捕获 `BizException`;其他异常记日志后返回通用提示 |
| `_batchDelete($service, $ids)` | 批量删除 | `Service::batchDelete($ids)` | ids 为空提示;同上 |
| `_export($service)` | 导出 | `Service::export()` | 捕获 `BizException`(如"无数据可导出") |
| `_import($service)` | 导入 | `Service::import()` | 文件格式/大小/MIME 校验 + 通用提示 |
/**
* 添加案例演示
*
* POST /example/add
* 对应权限:sys:example:add
*
* 调用 BaseController::_add(),内部流程:
* 1. 调用 ExampleLogic::add($data)
* 2. 触发 beforeAdd/afterAdd 钩子
* 3. 返回新记录 ID
*
* @return Json
*/
#[Log('案例演示-新增记录', Log::TYPE_ADD, '新增案例演示:{name}')]
#[Permission('sys:example:add', '添加案例演示')]
public function add(): Json
{
// getJsonBody() 获取前端 POST 提交的 JSON 数据
return parent::_add($this->logic, $this->getJsonBody());
}
响应封装方法:`$this->success($data, $msg)` / `$this->fail($msg, $code)` / `$this->paginate($result)`(委托给 `Support\Result`,见 [5.16 统一响应与异常抛出](/zh/core/exception.html))。
/**
* 更新案例演示
*
* PUT /example/update
* 对应权限:sys:example:update
*
* 从 JSON 请求体中读取 id,若缺失则直接返回失败。
*
* @return Json
*/
#[Log('案例演示-修改记录', Log::TYPE_UPDATE, '修改案例演示ID:{id}')]
#[Permission('sys:example:update', '修改案例演示')]
public function update(): Json
{
$data = $this->getJsonBody();
$id = $data['id'] ?? 0;
// 校验 ID 参数
if (empty($id)) {
return $this->fail('缺少id参数');
}
return parent::_edit($this->logic, $id, $data);
}
/**
* 删除案例演示
*
* DELETE /example/delete/{id}
* 对应权限:sys:example:delete
*
* 调用 BaseController::_remove(),内部流程:
* 1. 调用 ExampleLogic::delete($id)
* 2. 触发 beforeDelete/afterDelete 钩子
* 3. 设置 is_delete=1(软删除)
*
* @param int $id 案例演示ID
* @return Json
*/
#[Log('案例演示-删除记录', Log::TYPE_DELETE, '删除案例演示ID:{id}')]
#[Permission('sys:example:delete', '删除案例演示')]
public function delete(int $id): Json
{
return parent::_remove($this->logic, $id);
}
/**
* 批量删除案例演示
*
* DELETE /example/batchDelete
* 对应权限:sys:example:delete
*
* 从 JSON 请求体中读取 ids 数组。
*
* @return Json
*/
#[Log('案例演示-批量删除记录', Log::TYPE_DELETE)]
#[Permission('sys:example:delete', '删除案例演示')]
public function batchDelete(): Json
{
$ids = $this->getJsonBody()['ids'] ?? [];
return parent::_batchRemove($this->logic, $ids);
}
/**
* 导入案例演示
*
* POST /example/import
* 对应权限:sys:example:import
*
* 处理流程:
* 1. 从请求中获取上传文件
* 2. 校验文件扩展名(仅 xls、xlsx)
* 3. 调用 ExampleLogic::import() 解析并导入
*
* @return Json
*/
#[Log('案例演示-导入数据', Log::TYPE_IMPORT)]
#[Permission('sys:example:import', '导入案例演示')]
public function import(): Json
{
$file = $this->request->file('file');
if (!$file) {
return $this->fail('请选择文件');
}
$ext = strtolower($file->getOriginalExtension());
if (!in_array($ext, ['xls', 'xlsx'])) {
return $this->fail('仅支持 xls、xlsx 格式的文件');
}
try {
$result = $this->logic->import($file->getRealPath());
$msg = empty($result['errors'])
? "成功导入 {$result['count']} 条数据"
: "成功导入 {$result['count']} 条,失败 " . count($result['errors']) . " 条";
return $this->success($result, $msg);
} catch (\Exception $e) {
return $this->fail('导入失败: ' . $e->getMessage());
}
}
/**
* 导出案例演示
*
* GET /example/export
* 对应权限:sys:example:export
*
* @return Json
*/
#[Log('案例演示-导出数据', Log::TYPE_EXPORT)]
#[Permission('sys:example:export', '导出案例演示')]
public function export(): Json
{
try {
$filePath = $this->logic->export($this->getParams());
return $this->success($filePath, '导出成功');
} catch (\Exception $e) {
return $this->fail('导出失败: ' . $e->getMessage());
}
}
}
```
参数获取方法:`getPageParams()`(pageNo/pageSize,限 1~100)、`getJsonBody()`(JSON 请求体)、`getParams()`(query + body 合并)。
## 注解说明
### #[Log] 操作日志注解
### #[OperationLog] 操作日志注解
```php
#[Log('案例演示-新增记录', Log::TYPE_ADD, '新增案例演示:{name}')]
// │ │ │
// │ │ └── 描述(支持 {param} 占位符)
// │ └── 操作类型
// └── 操作标题
#[OperationLog('示例管理-新增', OperationLog::TYPE_ADD, '新增示例:{name}')]
// │ │ │
// │ │ └── 描述模板(支持 {param} 占位符,替换为请求参数值)
// │ └── 操作类型常量(TYPE_QUERY/ADD/UPDATE/DELETE/IMPORT/EXPORT 等 30 种)
// └── 操作标题
```
- 由 `log.operation` 中间件采集,响应后(terminable 阶段)落库;
- 写操作(POST/PUT/DELETE)始终记录,读操作按注解类型选择性记录。
### #[Permission] 权限注解
```php
#[Permission('sys:example:add', '添加案例演示')]
// │ │
// │ └── 权限名称
// └── 权限编码
#[Permission('sys:example:add', '添加示例')]
// │ │
// │ └── 权限名称(菜单管理中显示)
// └── 权限编码(菜单表 permission 字段)
```
## BaseController 通用方法
- 由 `auth.jwt` 中间件反射校验:解析注解 → 比对当前用户权限列表 → 无权限返回 `code=403`;
- 超级管理员(用户 ID=1)直接放行;
- 未标注 `#[Permission]` 的认证接口只需登录即可访问。
| 方法 | 用途 | 内部调用 |
|------|------|----------|
| `_index($logic)` | 分页查询 | `$logic->pageList()` |
| `_list($logic)` | 全量列表 | `$logic->allList()` |
| `_detail($logic, $id)` | 详情查询 | `$logic->detail($id)` |
| `_add($logic, $data)` | 新增 | `$logic->add($data)` |
| `_edit($logic, $id, $data)` | 修改 | `$logic->update($id, $data)` |
| `_remove($logic, $id)` | 删除 | `$logic->delete($id)` |
| `_batchRemove($logic, $ids)` | 批量删除 | `$logic->batchDelete($ids)` |
## 与手动构造响应的对比
```php
// ✅ 推荐:快捷方法(异常处理、日志、404 兜底全部内置)
public function page(): JsonResponse
{
return $this->_page($this->exampleService);
}
// 特殊场景:手动组织响应
public function stats(): JsonResponse
{
$data = $this->exampleService->stats();
if (! $data) {
return $this->fail('暂无统计数据', 404);
}
return $this->success($data);
}
```