Become a sponsor

概述
项目使用 PHP 8 原生注解(Attribute)实现声明式配置,目前有三个注解类,由 AttributeService 通过反射读取。注解标注在控制器方法或类上,中间件在请求处理过程中读取并执行对应逻辑(日志记录、权限校验、演示放行)。
| 注解 | 作用域 | 读取方 | 用途 | 必填参数 |
|---|---|---|---|---|
#[Log] | 方法 | LogMiddleware | 操作日志记录 | title |
#[Permission] | 方法/类 | AuthMiddleware | 权限校验 | code |
#[DemoAllow] | 方法 | DemoMiddleware | 演示环境放行 | 无 |
请求进入
│
▼
CorsMiddleware(全局)
│
▼
AuthMiddleware
│ 读取 #[Permission] 注解
│ 校验权限码 → 无权限返回 403
│
▼
TenantMiddleware
│
▼
DemoMiddleware
│ 读取 #[DemoAllow] 注解
│ 演示环境 + 写操作 + 无注解 → 拦截返回 403
│
▼
LogMiddleware
│ 读取 #[Log] 注解
│ 记录操作日志
│
▼
Controller 方法执行文件: app/attribute/Log.php读取方: LogMiddleware 作用域: 方法
#[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')]
// │ │ │
// │ │ └── 描述(支持 {param} 占位符)
// │ └── 操作类型(40种常量)
// └── 操作标题| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
title | string | ✓ | 操作标题,如"用户管理-新增记录" |
type | int | ✗ | 操作类型常量,默认 0(由中间件按 HTTP 方法推断) |
description | string | ✗ | 详细描述,支持 {param} 占位符 |
// 新增
#[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')]
// 修改
#[Log('用户管理-修改记录', Log::TYPE_UPDATE, '修改用户ID:{id}')]
// 删除
#[Log('用户管理-删除记录', Log::TYPE_DELETE, '删除用户ID:{id}')]
// 批量删除
#[Log('用户管理-批量删除记录', Log::TYPE_DELETE)]
// 导出(GET 请求也会记录)
#[Log('用户管理-导出数据', Log::TYPE_EXPORT)]
// 导入
#[Log('用户管理-导入数据', Log::TYPE_IMPORT, '通过Excel导入用户数据')]
// 重置密码
#[Log('用户管理-重置密码', Log::TYPE_RESET, '重置用户ID:{id}的密码')]
// 登录(TYPE_LOGIN)
#[Log('系统登录-用户登录', Log::TYPE_LOGIN)]
// 登出(TYPE_LOGOUT)
#[Log('系统登录-用户登出', Log::TYPE_LOGOUT)]
// 查询(GET 默认不记录,除非是导出/下载/导入类型)
#[Log('用户管理-查询分页记录', Log::TYPE_QUERY)]#[Log('文章管理-删除记录', Log::TYPE_DELETE, '删除文章ID:{id}')]
// {id} 会被替换为 $request->param('id') 的值
// 实际记录:"删除文章ID:42"| 常量 | 值 | 说明 | GET 是否记录 |
|---|---|---|---|
TYPE_ADD | 1 | 新增 | — |
TYPE_UPDATE | 2 | 修改 | — |
TYPE_DELETE | 3 | 删除 | — |
TYPE_QUERY | 4 | 查询 | ✗ |
TYPE_IMPORT | 5 | 导入 | ✓ |
TYPE_EXPORT | 6 | 导出 | ✓ |
TYPE_DOWNLOAD | 7 | 下载 | ✓ |
TYPE_APPROVE | 8 | 审批 | — |
TYPE_REJECT | 9 | 驳回 | — |
TYPE_SUBMIT | 10 | 提交 | — |
TYPE_WITHDRAW | 11 | 撤回 | — |
TYPE_LOGIN | 21 | 登录 | — |
TYPE_LOGOUT | 22 | 登出 | — |
TYPE_RESET | 24 | 重置 | — |
TYPE_OTHER | 99 | 其他 | — |
完整常量列表见 app/attribute/Log.php(共 40 种)。
文件: app/attribute/Permission.php读取方: AuthMiddleware 作用域: 方法 + 类(方法优先)
#[Permission('sys:user:add', '添加用户')]
// │ │
// │ └── 权限名称(用于提示和日志)
// └── 权限编码| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
code | string | ✓ | 权限编码,如 sys:user:add |
name | string | ✗ | 权限名称,用于提示和日志 |
// 方法级注解(最常用)
#[Permission('sys:user:add', '添加用户')]
public function add(): Json { ... }
#[Permission('sys:user:page', '用户分页')]
public function page(): Json { ... }
// 类级注解(该类所有方法都需此权限)
#[Permission('sys:user:manage', '用户管理')]
class UserController extends BaseController { ... }1. 方法上有 #[Permission] → 使用方法的权限码
2. 方法上无 #[Permission],类上有 → 使用类的权限码
3. 方法和类都没有 → 跳过权限校验(所有已登录用户可访问)uid=1 的用户为超级管理员,跳过所有权限校验。
AuthMiddleware 检测到 uid=1 时直接放行,不查询权限表。文件: app/attribute/DemoAllow.php读取方: DemoMiddleware 作用域: 方法 参数: 无
// 标注后,演示环境下该写操作不受拦截
#[DemoAllow]
public function refreshCache(): Json { ... }
#[DemoAllow]
public function login(): Json { ... }DemoMiddleware 检查逻辑:
1. APP_DEMO = false → 放行(非演示环境,env('app_demo'))
2. APP_DEMO = true + GET 请求 → 放行
3. APP_DEMO = true + 写操作(POST/PUT/DELETE)
├─ 方法有 #[DemoAllow] → 放行
└─ 方法无 #[DemoAllow] → 拦截,返回 403// 登录接口:演示环境也需要允许登录
#[DemoAllow]
public function login(): Json { ... }
// 缓存刷新:演示环境也需要允许
#[DemoAllow]
public function refreshCache(): Json { ... }
// 用户新增:演示环境禁止(无 #[DemoAllow])
public function add(): Json { ... }// 操作日志 + 权限校验(最常见组合)
#[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')]
#[Permission('sys:user:add', '添加用户')]
public function add(): Json { ... }
// 操作日志 + 权限校验 + 演示放行
#[Log('系统登录-用户登录', Log::TYPE_LOGIN)]
#[Permission('sys:login:do', '登录')]
#[DemoAllow]
public function login(): Json { ... }
// 仅权限校验(查询接口不需要日志)
#[Permission('sys:user:page', '用户分页')]
public function page(): Json { ... }
// 仅日志(不需要权限校验的公开接口)
#[Log('系统登录-获取验证码', Log::TYPE_OTHER)]
public function captcha(): Json { ... }文件: app/service/AttributeService.php职责: 通过反射读取注解,带缓存机制
| 方法 | 说明 | 返回值 |
|---|---|---|
getLog($class, $method) | 获取方法上的 Log 注解 | Log|null |
getPermission($class, $method) | 获取方法/类上的 Permission 注解 | Permission|null |
hasDemoAllow($class, $method) | 判断方法是否有 DemoAllow 注解 | bool |
// 获取方法上的 Log 注解
$log = AttributeService::getLog('app\controller\UserController', 'add');
if ($log) {
$title = $log->title; // '用户管理-新增记录'
$type = $log->type; // 1 (TYPE_ADD)
$desc = $log->description; // '新增用户:{username}'
}
// 获取方法上的 Permission 注解(方法优先,其次类)
$perm = AttributeService::getPermission('app\controller\UserController', 'add');
if ($perm) {
$code = $perm->code; // 'sys:user:add'
$name = $perm->name; // '添加用户'
}
// 判断是否有 DemoAllow 注解
$allowed = AttributeService::hasDemoAllow('app\controller\DictController', 'refreshCache');
// true / false缓存键:类名@方法名
缓存位置:static::$localCache(请求级缓存)
缓存策略:同一请求内不重复反射,跨请求重新解析
示例:
"app\controller\UserController@add" → Log 实例 + Permission 实例// 1. 定义注解类
// app/attribute/RateLimit.php
namespace app\attribute;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class RateLimit
{
public function __construct(
public int $max = 100, // 最大请求数
public int $window = 60, // 窗口期(秒)
) {}
}
// 2. 在控制器方法上使用
#[RateLimit(max: 10, window: 60)]
public function sendSms(): Json { ... }
// 3. 在中间件中读取
public function handle($request, \Closure $next)
{
$ref = new \ReflectionMethod($controller, $method);
$attrs = $ref->getAttributes(RateLimit::class);
if (!empty($attrs)) {
$rateLimit = $attrs[0]->newInstance();
// $rateLimit->max, $rateLimit->window
}
}| 实践 | 说明 |
|---|---|
| 注解即文档 | 通过注解即可了解接口的操作类型、权限要求 |
写操作必须标注 #[Log] | 删除、重置密码等敏感操作必须记录日志 |
| 权限码统一格式 | 使用 sys:{module}:{action} 格式 |
| 描述加关键参数 | 用 {param} 占位符记录业务主键,便于追溯 |
查询不标注 #[Log] | 普通查询不记录日志,避免日志膨胀 |
| 组合使用 | #[Log] + #[Permission] 组合是最常见的模式 |