ThinkPHP需手动实现字段级审计:更新时显式查询原始数据比对差异,过滤敏感字段和系统字段,JSON/datetime需标准化处理,密码等凭据仅标记变更不存值,软删除状态需单独监听,审计逻辑建议收口至控制器。

模型字段变更怎么自动记录到审计表
ThinkPHP 本身不内置字段级变更审计,得自己在 save() 或事件里抓差异。核心思路是:读取原始数据(从数据库查一次),和当前提交的数据做键值比对,只记录真正变过的字段。
常见错误是直接比对 $model->getData() 和 $model->getOriginal() ——但 getOriginal() 在新增场景下为空,在批量更新或手动赋值后也可能不准。稳妥做法是显式调用 find($id) 拿原始快照。
- 仅在更新操作(
update场景)触发审计,新增/删除走另一套逻辑 - 敏感字段列表建议硬编码在模型里,比如
protected $auditFields = ['username', 'email', 'status'],避免全量比对拖慢性能 - 注意时间字段(如
updated_at)别误判为变更项,比对前先从差异中过滤掉 - 审计日志建议单独建表,字段至少含:
table_name、record_id、field、old_value、new_value、operator_id、created_at
如何在 save() 前拦截并补全审计数据
最直接的位置是模型的 beforeWrite 钩子,但它拿不到旧值;更合适的是重写 save() 方法,或在控制器里统一处理。推荐后者——把审计逻辑收口,避免模型膨胀。
实操时别在 save() 内部再调 find(),容易引发事务死锁或 N+1。应该在调用 save() 前就准备好原始数据:
if ($user->isUpdate()) {
$original = User::find($user->id);
$diff = array_diff_assoc($user->getData(), $original->getData());
// 过滤非审计字段、系统字段
$auditData = array_intersect_key($diff, array_flip($user->auditFields));
// 写入 audit_log 表...
}
$user->save();
-
array_diff_assoc()能保留键名且比对值类型,比array_diff()更准 - 如果字段是 JSON 类型(如
profile),需用json_encode()标准化后再比,否则空格/换行会导致误判 - 遇到
datetime字段,记得用date('Y-m-d H:i:s')统一格式,数据库可能存微秒或时区偏移
软删除字段 status=0 变更要不要记审计
要记,但不能当成普通字段处理。因为 delete() 方法默认只改 delete_time,不碰 status;而业务层常自己设 status=0 来“逻辑禁用”,这属于关键状态跃迁。
这类字段必须单独监听,不能依赖通用字段比对逻辑:
- 在控制器里显式判断:
if (isset($data['status']) && $data['status'] != $original->status) - 状态变更建议附带原因字段(如
status_reason),审计表里留一列存文本说明 - 避免把
delete_time变更混进字段审计——它属于生命周期动作,应归入操作日志而非字段变更日志
审计数据里怎么安全存储密码类字段
绝对不存明文,也不存哈希值。哪怕只是记录“密码被修改”,也别把新旧哈希写进审计表——哈希碰撞风险低,但合规审计(如等保、GDPR)明确禁止日志留存认证凭据。
正确做法是:检测到 password 字段参与变更时,只记固定脱敏标记:
'old_value' => '***', 'new_value' => '***', 'field' => 'password'
- 连
strlen()都不要暴露(比如写成 '*** (6 chars)'),防止推断密码长度 - 如果业务要求记录“谁改了密码”,审计日志里只留
operator_id和created_at,其他字段全打码 - 数据库审计表加
COMMENT '密码字段仅记录变更事件,不存任何凭证内容',方便后续合规检查
字段级审计真正的难点不在代码,而在界定“什么是敏感”——同一个 phone 字段,在用户表里是敏感信息,在物流单里可能只是联系人备注。规则得按表、按场景配,别指望一套逻辑打天下。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











