本文目录导读:

在PHP项目中追溯操作日志的操作人员,通常需要结合用户认证系统、中间件和日志记录机制来实现,核心思路是:在用户操作的关键节点(增删改查),自动记录谁在什么时间通过什么IP对什么数据做了什么操作。
以下是几种常见的实现方案,从简单到复杂:
核心要素:确定“操作人员”是谁
必须有一套用户认证系统(Session、JWT、OAuth等),能够唯一标识当前登录用户。
// 假设使用 Session 或 Laravel/FastAdmin 等框架 $operatorId = $_SESSION['user_id'] ?? 0; // 用户ID $operatorName = $_SESSION['username'] ?? 'guest'; // 用户名
无法追溯的情况: 如果程序逻辑没有绑定用户身份(例如纯API key调用、后台定时任务、未登录状态查询),操作日志就只能记录一个“系统用户”或“未知”,无法精准定位自然人。
日志记录的内容(关键字段)
一个好的操作日志表通常包含以下字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT | 主键 |
| user_id | INT | 操作人员ID |
| username | VARCHAR | 操作人员姓名(冗余,方便查) |
| action | VARCHAR | 操作类型 (insert/update/delete/login) |
| table_name | VARCHAR | 操作的表或模块 |
| record_id | INT | 被操作的数据主键ID |
| old_data | TEXT | 修改前的数据JSON |
| new_data | TEXT | 修改后的数据JSON |
| ip_address | VARCHAR | 客户端IP |
| user_agent | TEXT | 浏览器/客户端标识 |
| created_at | DATETIME | 操作时间 |
| project | VARCHAR | 项目/模块标识(多项目共用时) |
几种实现方案
手动记录(最基础,适合小项目)
在每个关键方法(如用户修改、订单删除)中手动调用日志函数。
// 用户修改密码时
function updatePassword($userId, $newPassword) {
// 执行更新操作...
// 手动记录日志
$logData = [
'user_id' => $_SESSION['user_id'], // 当前操作人
'username' => $_SESSION['username'],
'action' => 'update',
'table_name' => 'users',
'record_id' => $userId,
'old_data' => json_encode(['password' => '***']),
'new_data' => json_encode(['password' => '***']),
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'created_at' => date('Y-m-d H:i:s'),
];
$db->insert('operation_logs', $logData);
}
缺点: 代码侵入性强,容易遗漏,且每个方法都要重复写日志代码。
AOP/中间件拦截(推荐,适用于框架项目)
利用框架的中间件(Middleware)或事件监听器,在路由/控制器执行前后自动记录日志,以Laravel为例:
步骤1:创建中间件
// app/Http/Middleware/OperationLog.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
class OperationLog
{
public function handle($request, Closure $next)
{
// 获取当前登录用户
$user = Auth::user();
// 记录请求基本信息(可选:后续在终止时记录更详细的内容)
$response = $next($request);
// 记录请求结束后的信息(例如响应状态码)
if ($user) {
Log::channel('operation')->info('操作日志', [
'user_id' => $user->id,
'user_name' => $user->name,
'method' => $request->method(),
'url' => $request->fullUrl(),
'params' => $request->except('password', 'token'), // 脱敏
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'timestamp' => now(),
]);
}
return $response;
}
}
步骤2:注册中间件(在 app/Http/Kernel.php 中添加到全局或特定路由组)
增强方案: 使用 Laravel的 Observers 或 Events 针对模型操作记录更细粒度的改动(old/new data)。
// App/Providers/EventServiceProvider.php
protected $listen = [
'App\Events\OrderCreated' => [
'App\Listeners\LogOrderCreation',
],
];
数据库触发器(DB Trigger,适合不允许修改代码的场景)
直接在数据库层配置触发器,当表发生INSERT/UPDATE/DELETE时,自动写入日志表。
问题: 数据库触发器无法直接获取PHP层的“当前登录用户ID”,需要PHP每次连接时设置一个会话变量:
// PHP代码在每次数据库查询前执行
$db->query("SET @current_user_id = " . intval($_SESSION['user_id']));
$db->query("SET @current_user_ip = '" . $db->real_escape_string($_SERVER['REMOTE_ADDR']) . "'");
然后在触发器中使用 @current_user_id 变量。
缺点: 依赖数据库,扩展性差,且如果多个请求并发,会话变量可能冲突(虽然MySQL的会话变量是连接级别的,但如果使用连接池且复用连接,可能会混乱)。
利用框架的Audit/Traits包
例如Laravel的 spatie/laravel-activitylog,直接安装配置即可:
// 模型中使用
use Spatie\Activitylog\Traits\LogsActivity;
class User extends Authenticatable
{
use LogsActivity;
protected static $logAttributes = ['name', 'email', 'role'];
protected static $logOnlyDirty = true;
protected static $logName = 'user';
public function getDescriptionForEvent(string $eventName): string
{
return "用户 " . $this->name . " 被" . eventNameMapping($eventName);
}
}
它会自动记录谁(通过Auth),什么时候,修改了什么。
谁负责记录“谁”?
最关键的一点:谁负责把当前用户ID写入日志?
- 如果是Web请求:用户登录后,Session/Auth对象全局可用,日志系统可以在日志记录时自动注入这个ID。
- 如果是API调用:需要确保每个API请求都携带了认证Token,中间件解析Token后提取User ID,注入到日志上下文中。
- 如果是CLI/Crontab:操作人员默认设置为
system或crontab,或者通过命令行参数传入--operator=xxx。 - 如果是代码修改直接操作数据库:无法追溯具体真人操作人员,只能记录一个泛化标识(如
manual_script)。
常见错误: 很多项目在记录日志时,忘记在日志记录的地方注入当前用户ID,导致所有日志都是 user_id=0 或 username=null。这是导致无法追溯的根本原因。 必须在日志记录的代码入口处(中间件、基类构造器)就确保 user_id 字段有值。
最佳实践总结(推荐做法)
- 使用框架的中间件机制:在全局中间件中统一获取当前用户ID,并存储到日志上下文(如
Log::withContext(['user_id' => auth()->id()]))。 - 在模型事件(Model Events)中记录:通过
Observer或Trait自动记录模型的修改(old/new data),而不要手动在控制器里写日志。 - 日志存储:建议使用专门的日志库(如
monolog),输出到文件或数据库,如果使用数据库,注意日志表的高频写入性能问题,可以定期归档。 - 不可篡改性:对重要操作日志,可考虑加签(HMAC)、写文件后立即加密、或存放到专门的日志系统(如ELK),至少要做到日志表只insert不update。
最终检查清单
- [ ] 用户登录后,所有请求是否能获取到
user_id? - [ ] 中间件或模型事件是否在每个操作中都执行了日志记录?
- [ ] 日志是否记录了“操作前数据”和“操作后数据”?(diff的关键)
- [ ] IP、User-Agent是否被正确记录?(辅助定位异常操作)
- [ ] 日志记录本身是否会抛异常导致主流程失败?(应该异步或try-catch)
按照以上方案实现,你就能够准确定位到:哪个人在哪个时间点通过哪个IP修改了哪条数据,从而实现完整的操作追溯。