12 KiB
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 |
演示环境放行 | 无 |
注解执行顺序
请求进入
│
▼
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,微秒级,无缓存)
作用域: 方法
基本用法
#[OperationLog('用户管理-新增', OperationLog::TYPE_ADD, '新增用户:{username}')]
// │ │ │
// │ │ └── 描述(支持 {param} 占位符)
// │ └── 操作类型(30 种常量)
// └── 操作标题
参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
title |
string | ✓ | 操作标题,如"用户管理-新增" |
type |
int | ✗ | 操作类型常量,默认 0(由中间件按 HTTP 方法推断) |
description |
string | ✗ | 详细描述,支持 {param} 占位符 |
构造函数签名(来自项目实际代码):
public function __construct(
public string $title,
public int $type = 0,
public string $description = '',
) {}
使用示例
// 新增 / 修改 / 删除
#[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)]
占位符替换
#[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 操作日志。)
#[Permission] 权限注解
文件: app/Attributes/Permission.php
读取方: Authenticate 中间件(ReflectionMethod)
作用域: 方法
基本用法
#[Permission('sys:user:add', '添加用户')]
// │ │
// │ └── 权限名称(菜单管理中显示、403 消息附带编码)
// └── 权限编码
参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
code |
string | ✓ | 权限编码,如 sys:user:add |
name |
string | ✗ | 权限名称,用于提示和菜单展示 |
使用示例
// 方法级注解(最常用)
#[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 { ... }
校验规则
1. 方法上有 #[Permission] → 使用方法的权限编码
2. 方法上无注解 → 跳过权限校验(所有已登录用户可访问)
3. uid=1 超级管理员 → 直接放行,不查询权限
4. 权限编码需与菜单表 permission 字段一致(菜单即权限)
详见 5.4 权限管理(RBAC)。
#[DemoAllow] 演示放行注解
文件: app/Attributes/DemoAllow.php
读取方: DemoMiddleware(反射读取,异常静默视为未标注)
作用域: 方法(仅 TARGET_METHOD,不支持类级)
参数: 无(标记型注解)
基本用法
// 标注后,演示环境下该写操作不受拦截
#[DemoAllow]
public function refreshCache(): JsonResponse { ... }
工作原理
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
典型场景
// 缓存刷新:仅清缓存不改业务数据,演示环境放行
#[OperationLog('字典管理-刷新缓存', OperationLog::TYPE_CLEAR)]
#[Permission('sys:dict:update', '修改字典')]
#[DemoAllow]
public function refreshCache(): JsonResponse { ... }
// 用户新增:演示环境禁止(无 #[DemoAllow])
public function add(UserRequest $request): JsonResponse { ... }
详见 5.7 演示模式。
#[WithoutOperationLog] 跳过操作日志注解
文件: app/Attributes/WithoutOperationLog.php
读取方: LogOperation 中间件(反射读取,异常静默视为未标注)
作用域: 方法(仅 TARGET_METHOD,不支持类级)
参数: 无(标记型注解)
基本用法
// 标注后,该方法不写入 OperationLog(写操作同样跳过)
#[WithoutOperationLog]
public function logout(Request $request): JsonResponse { ... }
工作原理
LogOperation 判定逻辑:
1. 方法有 #[WithoutOperationLog] → 不记录(优先级最高,先于记录规则)
2. 否则按记录规则判定:
├─ 写方法(POST/PUT/DELETE/PATCH)→ 一律记录
├─ GET + 导出/导入/下载类型注解 → 记录
└─ 其它 → 不记录
典型场景
已有专门日志载体的接口——登出的记录写入登录日志(login_log,log_type=2),不再重复产生操作日志:
#[DemoAllow]
#[WithoutOperationLog]
public function logout(Request $request): JsonResponse { ... }
注解组合使用
// 操作日志 + 权限校验(最常见组合)
#[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 读取注解(项目没有独立的注解读取服务,逻辑内聚在各中间件中):
// 通用读取模式(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 实际代码):
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 = '',
) {}
}
新建自定义注解的三个步骤:
- 定义注解类:在
app/Attributes/下按上述结构创建类(TARGET_METHOD标注在方法上,构造参数即注解参数); - 在控制器方法上使用:如
#[Permission('sys:user:add', '添加用户')]; - 在中间件中读取:用上一节的反射模式
getAttributes(你的注解类::class)读取并处理。
最佳实践
| 实践 | 说明 |
|---|---|
| 注解即文档 | 通过注解即可了解接口的操作类型、权限要求 |
写操作必须标注 #[OperationLog] |
删除、重置密码等敏感操作必须记录日志 |
| 权限码统一格式 | 使用 sys:{module}:{action} 格式,与菜单 permission 一致 |
| 描述加关键参数 | 用 {param} 占位符记录业务主键,便于追溯 |
| 查询注解仅作语义标注 | TYPE_QUERY 不落库,勿依赖其做审计 |
| 组合使用 | #[OperationLog] + #[Permission] 组合是最常见的模式 |