# 5.17 注解系统总览 :::tip 概述 项目使用 PHP 8 原生注解(Attribute)实现声明式配置,目前有四个注解类(`app/Attributes/`)。注解标注在控制器方法上,中间件在请求处理过程中通过**反射(ReflectionMethod)读取**并执行对应逻辑(日志记录、跳过日志、权限校验、演示放行)——业务代码零侵入。 ::: ## 注解清单 | 注解 | 文件 | 作用域 | 读取方 | 用途 | 必填参数 | |------|------|--------|--------|------|---------| | `#[OperationLog]` | `OperationLog.php`(275 行) | 方法 | `LogOperation` 中间件 | 操作日志记录 | title | | `#[WithoutOperationLog]` | `WithoutOperationLog.php` | 方法 | `LogOperation` 中间件 | 跳过操作日志(标注即不记录) | 无 | | `#[Permission]` | `Permission.php` | 方法 | `Authenticate` 中间件 | 权限校验 | code | | `#[DemoAllow]` | `DemoAllow.php` | 方法 | `DemoMiddleware` | 演示环境放行 | 无 | ## 注解执行顺序 ```text 请求进入 │ ▼ ProxyMiddleware / Cors / ForceJsonResponse(全局 api 组) │ ▼ Authenticate(auth.jwt) │ 反射读取 #[Permission] 注解(方法级) │ 校验权限码 → 无权限返回 HTTP 403 │ ▼ ResolveTenant(tenant) │ ▼ DemoMiddleware(demo,认证路由组内) │ 反射读取 #[DemoAllow] 注解 │ 演示环境 + 写操作 + 无注解 → 拦截返回 403 │ ▼ LogOperation(log.operation) │ handle:反射读取 #[WithoutOperationLog](标注即跳过)与 #[OperationLog] 注解,判断是否记录 │ terminate:响应后写入操作日志 │ ▼ Controller 方法执行 ``` ## #[OperationLog] 操作日志注解 **文件:** `app/Attributes/OperationLog.php` **读取方:** `LogOperation` 中间件(`ReflectionMethod`,微秒级,无缓存) **作用域:** 方法 ### 基本用法 ```php #[OperationLog('用户管理-新增', OperationLog::TYPE_ADD, '新增用户:{username}')] // │ │ │ // │ │ └── 描述(支持 {param} 占位符) // │ └── 操作类型(30 种常量) // └── 操作标题 ``` ### 参数说明 | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `title` | string | ✓ | 操作标题,如"用户管理-新增" | | `type` | int | ✗ | 操作类型常量,默认 0(由中间件按 HTTP 方法推断) | | `description` | string | ✗ | 详细描述,支持 `{param}` 占位符 | 构造函数签名(来自项目实际代码): ```php public function __construct( public string $title, public int $type = 0, public string $description = '', ) {} ``` ### 使用示例 ```php // 新增 / 修改 / 删除 #[OperationLog('用户管理-新增', OperationLog::TYPE_ADD, '新增用户:{username}')] #[OperationLog('用户管理-修改', OperationLog::TYPE_UPDATE, '修改用户ID:{id}')] #[OperationLog('用户管理-删除', OperationLog::TYPE_DELETE, '删除用户ID:{id}')] // 批量删除 #[OperationLog('用户管理-批量删除', OperationLog::TYPE_DELETE)] // 导出(GET 请求也会记录) #[OperationLog('用户管理-导出数据', OperationLog::TYPE_EXPORT)] // 导入 #[OperationLog('用户管理-导入数据', OperationLog::TYPE_IMPORT, '通过Excel导入用户数据')] // 重置密码 #[OperationLog('用户管理-重置密码', OperationLog::TYPE_RESET, '重置用户ID:{id}的密码')] // 清理缓存 #[OperationLog('字典管理-刷新缓存', OperationLog::TYPE_CLEAR)] // 查询(GET + TYPE_QUERY 不落库,仅作语义标注) #[OperationLog('用户管理-查询分页', OperationLog::TYPE_QUERY)] ``` ### 占位符替换 ```php #[OperationLog('文章管理-删除', OperationLog::TYPE_DELETE, '删除文章ID:{id}')] // {id} 会被替换为请求参数中 id 的值 // 实际记录:"删除文章ID:42" ``` ### 操作类型常量(30 种) | 常量 | 值 | 说明 | GET 是否落库 | |------|-----|------|-------------| | `TYPE_ADD` | 1 | 新增 | — | | `TYPE_UPDATE` | 2 | 修改 | — | | `TYPE_DELETE` | 3 | 删除 | — | | `TYPE_QUERY` | 4 | 查询 | ✗ | | `TYPE_IMPORT` | 5 | 导入 | ✓ | | `TYPE_EXPORT` | 6 | 导出 | ✓ | | `TYPE_APPROVE` | 7 | 审批 | — | | `TYPE_REJECT` | 8 | 驳回 | — | | `TYPE_SUBMIT` | 9 | 提交 | — | | `TYPE_WITHDRAW` | 10 | 撤回 | — | | `TYPE_CLOSE` / `TYPE_OPEN` | 11 / 12 | 关闭 / 开启 | — | | `TYPE_ENABLE` / `TYPE_DISABLE` | 13 / 14 | 启用 / 禁用 | — | | `TYPE_UPLOAD` / `TYPE_DOWNLOAD` | 15 / 16 | 上传 / 下载 | 下载 ✓ | | `TYPE_SEND` / `TYPE_SYNC` | 17 / 18 | 发送 / 同步 | — | | `TYPE_GENERATE` / `TYPE_CLEAR` | 19 / 20 | 生成 / 清理 | — | | `TYPE_RESTORE` | 21 | 恢复 | — | | `TYPE_LOGIN` / `TYPE_LOGOUT` | 22 / 23 | 登录 / 登出 | — | | `TYPE_RESET` | 24 | 重置 | — | | `TYPE_ASSIGN` / `TYPE_AUTHORIZE` | 25 / 26 | 分配 / 授权 | — | | `TYPE_EXECUTE` | 27 | 执行 | — | | `TYPE_PAUSE` / `TYPE_RESUME` | 28 / 29 | 暂停 / 恢复执行 | — | | `TYPE_OTHER` | 99 | 其他 | — | (完整注释见 `app/Attributes/OperationLog.php`;GET 仅 IMPORT/EXPORT/DOWNLOAD 三类落库,见 [5.14 操作日志](/zh/core/operation-log.html)。) ## #[Permission] 权限注解 **文件:** `app/Attributes/Permission.php` **读取方:** `Authenticate` 中间件(`ReflectionMethod`) **作用域:** 方法 ### 基本用法 ```php #[Permission('sys:user:add', '添加用户')] // │ │ // │ └── 权限名称(菜单管理中显示、403 消息附带编码) // └── 权限编码 ``` ### 参数说明 | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `code` | string | ✓ | 权限编码,如 `sys:user:add` | | `name` | string | ✗ | 权限名称,用于提示和菜单展示 | ### 使用示例 ```php // 方法级注解(最常用) #[Permission('sys:user:page', '用户分页')] public function page(): JsonResponse { ... } // 与日志组合(最常见模式) #[OperationLog('用户管理-新增', OperationLog::TYPE_ADD, '新增用户:{username}')] #[Permission('sys:user:add', '添加用户')] public function add(UserRequest $request): JsonResponse { ... } // 不标注 = 该接口只需登录,无需特定权限 public function getOptions(): JsonResponse { ... } ``` ### 校验规则 ```text 1. 方法上有 #[Permission] → 使用方法的权限编码 2. 方法上无注解 → 跳过权限校验(所有已登录用户可访问) 3. uid=1 超级管理员 → 直接放行,不查询权限 4. 权限编码需与菜单表 permission 字段一致(菜单即权限) ``` 详见 [5.4 权限管理(RBAC)](/zh/core/rbac.html)。 ## #[DemoAllow] 演示放行注解 **文件:** `app/Attributes/DemoAllow.php` **读取方:** `DemoMiddleware`(反射读取,异常静默视为未标注) **作用域:** 方法(仅 `TARGET_METHOD`,不支持类级) **参数:** 无(标记型注解) ### 基本用法 ```php // 标注后,演示环境下该写操作不受拦截 #[DemoAllow] public function refreshCache(): JsonResponse { ... } ``` ### 工作原理 ```text DemoMiddleware 检查逻辑: 1. DEMO_MODE = false → 放行(非演示环境,config('app.demo_mode')) 2. DEMO_MODE = true + GET 请求 → 放行 3. DEMO_MODE = true + 写操作(POST/PUT/DELETE/PATCH) ├─ 方法有 #[DemoAllow] → 放行 └─ 方法无 #[DemoAllow] → 拦截,返回 HTTP 403 ``` ### 典型场景 ```php // 缓存刷新:仅清缓存不改业务数据,演示环境放行 #[OperationLog('字典管理-刷新缓存', OperationLog::TYPE_CLEAR)] #[Permission('sys:dict:update', '修改字典')] #[DemoAllow] public function refreshCache(): JsonResponse { ... } // 用户新增:演示环境禁止(无 #[DemoAllow]) public function add(UserRequest $request): JsonResponse { ... } ``` 详见 [5.7 演示模式](/zh/core/demo-mode.html)。 ## #[WithoutOperationLog] 跳过操作日志注解 **文件:** `app/Attributes/WithoutOperationLog.php` **读取方:** `LogOperation` 中间件(反射读取,异常静默视为未标注) **作用域:** 方法(仅 `TARGET_METHOD`,不支持类级) **参数:** 无(标记型注解) ### 基本用法 ```php // 标注后,该方法不写入 OperationLog(写操作同样跳过) #[WithoutOperationLog] public function logout(Request $request): JsonResponse { ... } ``` ### 工作原理 ```text LogOperation 判定逻辑: 1. 方法有 #[WithoutOperationLog] → 不记录(优先级最高,先于记录规则) 2. 否则按记录规则判定: ├─ 写方法(POST/PUT/DELETE/PATCH)→ 一律记录 ├─ GET + 导出/导入/下载类型注解 → 记录 └─ 其它 → 不记录 ``` ### 典型场景 已有专门日志载体的接口——登出的记录写入登录日志(`login_log`,`log_type=2`),不再重复产生操作日志: ```php #[DemoAllow] #[WithoutOperationLog] public function logout(Request $request): JsonResponse { ... } ``` ## 注解组合使用 ```php // 操作日志 + 权限校验(最常见组合) #[OperationLog('用户管理-新增', OperationLog::TYPE_ADD, '新增用户:{username}')] #[Permission('sys:user:add', '添加用户')] public function add(UserRequest $request): JsonResponse { ... } // 仅权限校验(查询接口不需要落日志) #[Permission('sys:user:page', '用户分页')] public function page(): JsonResponse { ... } // 操作日志 + 权限 + 演示放行(三注解齐全的特殊场景) #[OperationLog('字典管理-刷新缓存', OperationLog::TYPE_CLEAR)] #[Permission('sys:dict:update', '修改字典')] #[DemoAllow] public function refreshCache(): JsonResponse { ... } // 演示放行 + 跳过操作日志(登出:演示环境可退出,记录只写登录日志) #[DemoAllow] #[WithoutOperationLog] public function logout(Request $request): JsonResponse { ... } ``` ## 反射读取机制 三个中间件均直接使用 `ReflectionMethod` 读取注解(项目**没有**独立的注解读取服务,逻辑内聚在各中间件中): ```php // 通用读取模式(Authenticate / LogOperation / DemoMiddleware 同款) $route = $request->route(); $controllerClass = $route->getControllerClass(); $method = $route->getActionMethod(); $reflection = new \ReflectionMethod($controllerClass, $method); $attrs = $reflection->getAttributes(Permission::class); // 换成目标注解类 if (! empty($attrs)) { $permission = $attrs[0]->newInstance(); // $permission->code / ->name } ``` :::tip 性能说明 反射读取开销为微秒级(单请求仅 2~3 次方法反射),且中间件对注解缺失场景做了静默降级(反射失败视为无注解,不阻断请求)——无需缓存,避免开发阶段改注解后需等缓存过期。 ::: ## 创建自定义注解 注解类结构参考(摘自 `app/Attributes/Permission.php` 实际代码): ```php namespace App\Attributes; use Attribute; #[Attribute(Attribute::TARGET_METHOD)] class Permission { /** * @param string $code 权限编码(鉴权唯一依据,如 sys:user:add) * @param string $name 权限名称(用于展示,如"添加用户") * @param string $description 权限描述(可选,补充说明) */ public function __construct( public string $code, public string $name = '', public string $description = '', ) {} } ``` 新建自定义注解的三个步骤: 1. **定义注解类**:在 `app/Attributes/` 下按上述结构创建类(`TARGET_METHOD` 标注在方法上,构造参数即注解参数); 2. **在控制器方法上使用**:如 `#[Permission('sys:user:add', '添加用户')]`; 3. **在中间件中读取**:用上一节的反射模式 `getAttributes(你的注解类::class)` 读取并处理。 ## 最佳实践 | 实践 | 说明 | |------|------| | 注解即文档 | 通过注解即可了解接口的操作类型、权限要求 | | 写操作必须标注 `#[OperationLog]` | 删除、重置密码等敏感操作必须记录日志 | | 权限码统一格式 | 使用 `sys:{module}:{action}` 格式,与菜单 `permission` 一致 | | 描述加关键参数 | 用 `{param}` 占位符记录业务主键,便于追溯 | | 查询注解仅作语义标注 | `TYPE_QUERY` 不落库,勿依赖其做审计 | | 组合使用 | `#[OperationLog]` + `#[Permission]` 组合是最常见的模式 |