核心是在beforeUpdate事件中用origin()对比旧值与当前data,仅当权限字段实际变更时记录结构化日志;日志须含operator_id、target_table、target_id、changed_fields、request_url等字段,并与主业务同事务写入数据库,不可用文件日志。

ThinkPHP模型事件里怎么捕获权限字段修改
权限变更日志的核心,是准确识别 auth_rule、role_auth 或用户表中权限相关字段(比如 role_id、permission_json)是否真的被改了,而不是每次 save() 都记一条。ThinkPHP 的 beforeWrite 或 afterUpdate 事件本身不提供“旧值”,得自己捞。
实操建议:
- 在模型的
beforeUpdate事件里,用$this->origin()拿原始数据,再和当前$this->data对比关键字段,例如:if ($this->origin('role_id') != $this->data['role_id']) { /* 记日志 */ } - 别依赖
afterUpdate—— 此时数据已写库,origin()可能为空或不准,尤其批量更新或手动调用update()时 - 如果权限是通过中间表(如
user_role)维护,模型事件监听不到,得在关联操作逻辑里手动触发日志记录
日志内容该存哪些字段才够排查问题
只记“用户A改了权限”没用,线上出问题时你得快速还原:谁在什么时间、通过哪个接口、把哪条记录的哪个字段从什么值改成什么值。
必须包含的字段:
立即学习“PHP免费学习笔记(深入)”;
-
operator_id:操作人ID(注意不是当前登录用户ID,而是实际执行修改的账号,比如后台管理员) -
target_table和target_id:明确是改了users表的第582条,还是roles表的第7条 -
changed_fields:JSON 格式,例如{"role_id": {"old": 3, "new": 5}},别只存字符串 “role_id changed” -
request_url和request_method:方便追溯到具体接口,比如/admin/role/assign
漏掉 request_url 或用 $_SERVER['REQUEST_URI'] 硬取,后期查日志会卡在“不知道这个变更来自哪个功能按钮”。
并发修改下日志记录失败或重复怎么办
多个管理员同时编辑同一个用户权限时,beforeUpdate 事件可能被多次触发,但数据库事务还没提交,origin() 取到的旧值可能一致,导致日志误判为“未变更”;或者更糟——事务回滚了,日志却已落库,变成脏数据。
稳妥做法:
- 日志写入必须和主业务在**同一事务内**,用
Db::transaction()包裹整个操作,包括日志insert() - 不要用异步队列单独处理日志——队列消费延迟或失败会导致日志缺失,且无法回滚
- 避免在
afterWrite里写日志:此时事务可能已提交,但若后续代码抛异常,主操作回滚而日志不回滚,状态不一致
用 Log::write() 记权限日志行不行
不行。文件日志(Log::write())没有结构化字段、没法按 operator_id 或 target_id 快速检索,也难以对接审计系统。线上排查时翻 runtime/log/ 下一堆文本,效率极低。
正确路径:
- 必须写进数据库表(如
sys_auth_log),字段设计支持索引查询 - 如果已有 ELK 或类似日志平台,可用
think-swoole或自定义日志驱动把结构化数据推过去,但底层仍需先保证 DB 写入可靠 - 别为了“轻量”用
file_put_contents()手写日志文件——权限变更属于敏感操作,必须可追溯、可防篡改、可审计
最常被跳过的一步:没给日志表的 created_at 字段加索引。万一日志量上来,按时间范围查最近10条变更都要秒级响应,没索引直接卡死。



















