PHP 操作日志怎么做

wen PHP项目 5

本文目录导读:

PHP 操作日志怎么做

  1. 基础文件日志方案
  2. 数据库日志方案
  3. 框架集成方案
  4. 完整日志系统示例
  5. 最佳实践建议

我来介绍几种 PHP 操作日志的实现方案,从简单到复杂:

基础文件日志方案

简单文件日志

<?php
class SimpleLogger {
    private $logPath;
    public function __construct($logPath = 'logs/') {
        $this->logPath = $logPath;
        if (!is_dir($logPath)) {
            mkdir($logPath, 0777, true);
        }
    }
    public function write($message, $type = 'INFO') {
        $date = date('Y-m-d H:i:s');
        $logLine = sprintf(
            "[%s] %s: %s %s\n",
            $date,
            $type,
            $message,
            $this->getClientInfo()
        );
        $fileName = $this->logPath . date('Y-m-d') . '.log';
        file_put_contents($fileName, $logLine, FILE_APPEND | LOCK_EX);
    }
    private function getClientInfo() {
        $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        $uri = $_SERVER['REQUEST_URI'] ?? 'unknown';
        return "[IP: $ip] [URI: $uri]";
    }
}
// 使用示例
$logger = new SimpleLogger();
$logger->write('用户登录成功', 'INFO');
$logger->write('登录失败,密码错误', 'WARNING');
?>

数据库日志方案

日志表结构

CREATE TABLE `operation_logs` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `username` varchar(50) DEFAULT NULL,
  `module` varchar(50) DEFAULT NULL,
  `action` varchar(50) DEFAULT NULL,
  `description` text,
  `request_data` text,
  `response_data` text,
  `ip` varchar(50) DEFAULT NULL,
  `user_agent` varchar(255) DEFAULT NULL,
  `execution_time` int(11) DEFAULT NULL,
  `created_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_module` (`module`),
  KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

数据库日志类

<?php
class DbLogger {
    private $db;
    public function __construct($db) {
        $this->db = $db;
    }
    public function log($module, $action, $description, $requestData = null, $responseData = null) {
        $logData = [
            'user_id' => $_SESSION['user_id'] ?? null,
            'username' => $_SESSION['username'] ?? 'guest',
            'module' => $module,
            'action' => $action,
            'description' => $description,
            'request_data' => $requestData ? json_encode($requestData) : null,
            'response_data' => $responseData ? json_encode($responseData) : null,
            'ip' => $this->getClientIp(),
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
            'execution_time' => $this->getExecutionTime(),
            'created_at' => date('Y-m-d H:i:s')
        ];
        $sql = "INSERT INTO operation_logs 
                (user_id, username, module, action, description, request_data, 
                 response_data, ip, user_agent, execution_time, created_at) 
                VALUES 
                (:user_id, :username, :module, :action, :description, :request_data, 
                 :response_data, :ip, :user_agent, :execution_time, :created_at)";
        $stmt = $this->db->prepare($sql);
        return $stmt->execute($logData);
    }
    private function getClientIp() {
        if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
            return $_SERVER['HTTP_CLIENT_IP'];
        } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            return $_SERVER['HTTP_X_FORWARDED_FOR'];
        } else {
            return $_SERVER['REMOTE_ADDR'] ?? '';
        }
    }
    private function getExecutionTime() {
        if (defined('START_TIME')) {
            return round((microtime(true) - START_TIME) * 1000);
        }
        return 0;
    }
    public function getLogs($filters = [], $page = 1, $perPage = 20) {
        // 查询日志的实现
        $offset = ($page - 1) * $perPage;
        $where = [];
        $params = [];
        if (!empty($filters['user_id'])) {
            $where[] = "user_id = :user_id";
            $params[':user_id'] = $filters['user_id'];
        }
        if (!empty($filters['module'])) {
            $where[] = "module = :module";
            $params[':module'] = $filters['module'];
        }
        if (!empty($filters['start_date'])) {
            $where[] = "created_at >= :start_date";
            $params[':start_date'] = $filters['start_date'];
        }
        if (!empty($filters['end_date'])) {
            $where[] = "created_at <= :end_date";
            $params[':end_date'] = $filters['end_date'];
        }
        $whereSql = $where ? "WHERE " . implode(" AND ", $where) : '';
        $countSql = "SELECT COUNT(*) as total FROM operation_logs $whereSql";
        $countStmt = $this->db->prepare($countSql);
        $countStmt->execute($params);
        $total = $countStmt->fetchColumn();
        $sql = "SELECT * FROM operation_logs 
                $whereSql 
                ORDER BY created_at DESC 
                LIMIT $perPage OFFSET $offset";
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        $logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
        return [
            'total' => $total,
            'page' => $page,
            'per_page' => $perPage,
            'data' => $logs
        ];
    }
}
?>

框架集成方案

Laravel 中间件方案

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
class OperationLogMiddleware
{
    public function handle($request, Closure $next)
    {
        $startTime = microtime(true);
        // 记录请求开始
        $response = $next($request);
        // 记录请求结束
        $executionTime = microtime(true) - $startTime;
        // 只记录操作型请求
        if ($request->method() != 'GET') {
            Log::channel('operation')->info('用户操作', [
                'user_id' => Auth::id(),
                'method' => $request->method(),
                'path' => $request->path(),
                'data' => $request->all(),
                'response_status' => $response->getStatusCode(),
                'execution_time' => $executionTime * 1000 . 'ms',
                'ip' => $request->ip()
            ]);
        }
        return $response;
    }
}
?>

ThinkPHP 的行为钩子方案

<?php
namespace app\behavior;
use think\facade\Log;
use think\facade\Request;
class OperationLog
{
    public function run()
    {
        $data = [
            'url' => Request::url(),
            'method' => Request::method(),
            'data' => Request::param(),
            'ip' => Request::ip(),
            'time' => date('Y-m-d H:i:s')
        ];
        Log::write('操作日志: ' . json_encode($data), 'info');
    }
}
?>

完整日志系统示例

<?php
class OperationLogSystem {
    private $db;
    private $cache;
    private $config;
    public function __construct($db) {
        $this->db = $db;
        $this->config = [
            'log_methods' => ['POST', 'PUT', 'DELETE'],
            'exclude_urls' => ['/login', '/logout'],
            'sensitive_data' => ['password', 'token', 'card_number'],
            // 日志保留天数
            'retention_days' => 30
        ];
    }
    public function record($module, $action, $description = '') {
        try {
            // 过滤敏感字段
            $requestData = $this->filterSensitiveData($_POST);
            $responseData = $this->getResponseData();
            $logData = [
                'user_id' => $this->getCurrentUserId(),
                'module' => $module,
                'action' => $action,
                'description' => $description,
                'request_data' => json_encode($requestData),
                'response_data' => $responseData,
                'ip' => $this->getRealIp(),
                'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
                'request_url' => $_SERVER['REQUEST_URI'] ?? '',
                'execution_time' => $this->getExecTime(),
                'created_at' => date('Y-m-d H:i:s')
            ];
            // 写入日志
            $this->writeToDatabase($logData);
            // 同时写入文件备份
            $this->writeToFile($logData);
        } catch (Exception $e) {
            // 日志记录失败不影响主流程
            Log::error('日志记录失败: ' . $e->getMessage());
        }
    }
    private function filterSensitiveData($data) {
        foreach ($this->config['sensitive_data'] as $field) {
            if (isset($data[$field])) {
                $data[$field] = '***';
            }
        }
        return $data;
    }
    private function writeToDatabase($logData) {
        $sql = "INSERT INTO operation_logs 
                (user_id, module, action, description, request_data, response_data,
                 ip, user_agent, request_url, execution_time, created_at) 
                VALUES (:user_id, :module, :action, :description, :request_data, :response_data,
                        :ip, :user_agent, :request_url, :execution_time, :created_at)";
        $stmt = $this->db->prepare($sql);
        $stmt->execute($logData);
    }
    private function writeToFile($logData) {
        $logDir = 'logs/';
        if (!is_dir($logDir)) {
            mkdir($logDir, 0777, true);
        }
        $filename = $logDir . date('Y-m-d') . '.json';
        $logLine = json_encode($logData, JSON_UNESCAPED_UNICODE) . PHP_EOL;
        file_put_contents($filename, $logLine, FILE_APPEND | LOCK_EX);
    }
    private function getRealIp() {
        // IP获取逻辑
        $ip = $_SERVER['REMOTE_ADDR'] ?? '';
        if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
            $ip = trim($ips[0]);
        }
        return $ip;
    }
    public function cleanupExpiredLogs() {
        $expiredDate = date('Y-m-d', strtotime("-{$this->config['retention_days']} days"));
        $sql = "DELETE FROM operation_logs WHERE created_at < ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$expiredDate]);
        // 清理文件
        $this->cleanupFiles($expiredDate);
    }
    private function cleanupFiles($expiredDate) {
        $files = glob('logs/*.json');
        foreach ($files as $file) {
            $fileDate = basename($file, '.json');
            if ($fileDate < $expiredDate) {
                unlink($file);
            }
        }
    }
}
// 使用示例
$logger = new OperationLogSystem($db);
$logger->record('user', 'add', '新增用户');
?>

最佳实践建议

日志管理后台视图

<!-- 日志查询页面 -->
<div class="container">
    <h2>操作日志</h2>
    <!-- 搜索过滤 -->
    <form class="mb-3">
        <div class="row">
            <div class="col-md-3">
                <input type="text" name="username" class="form-control" placeholder="用户名">
            </div>
            <div class="col-md-2">
                <select name="module" class="form-control">
                    <option value="">全部模块</option>
                    <option value="user">用户管理</option>
                    <option value="role">角色管理</option>
                    <option value="system">系统设置</option>
                </select>
            </div>
            <div class="col-md-3">
                <div class="input-group">
                    <input type="date" name="start_date" class="form-control">
                    <span>-</span>
                    <input type="date" name="end_date" class="form-control">
                </div>
            </div>
            <div class="col-md-2">
                <button type="submit" class="btn btn-primary">搜索</button>
            </div>
        </div>
    </form>
    <!-- 日志列表 -->
    <table class="table table-bordered">
        <thead>
            <tr>
                <th>ID</th>
                <th>用户</th>
                <th>模块</th>
                <th>操作</th>
                <th>描述</th>
                <th>IP</th>
                <th>时间</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            <?php foreach ($logs as $log): ?>
            <tr>
                <td><?= $log['id'] ?></td>
                <td><?= htmlspecialchars($log['username']) ?></td>
                <td><?= htmlspecialchars($log['module']) ?></td>
                <td><?= htmlspecialchars($log['action']) ?></td>
                <td><?= htmlspecialchars($log['description']) ?></td>
                <td><?= $log['ip'] ?></td>
                <td><?= $log['created_at'] ?></td>
                <td>
                    <button class="btn btn-sm btn-info" 
                            onclick="viewDetail(<?= $log['id'] ?>)">详情</button>
                </td>
            </tr>
            <?php endforeach; ?>
        </tbody>
    </table>
    <!-- 分页 -->
    <?= $pagination ?>
</div>

性能优化建议

// 1. 异步日志处理
public function asyncLog($data) {
    $redis = new Redis();
    $redis->lpush('log_queue', json_encode($data));
    // 后台消费者处理
    // 可以定期从队列取出日志写入数据库
}
// 2. 日志缓存
public function cachedLog($data) {
    $cache = new Redis();
    $cache->sAdd('operation_logs_' . date('Y-m-d'), json_encode($data));
    // 定时批量写入
    if ($cache->sCard('operation_logs_' . date('Y-m-d')) >= 100) {
        $this->batchWriteToDB($cache->sMembers('operation_logs_' . date('Y-m-d')));
        $cache->del('operation_logs_' . date('Y-m-d'));
    }
}
// 3. 日志级别控制
class LogLevel {
    const LOG_DEBUG = 'DEBUG';
    const LOG_INFO = 'INFO';
    const LOG_WARNING = 'WARNING';
    const LOG_ERROR = 'ERROR';
    private $minLevel;
    public function shouldLog($level) {
        $levels = [self::LOG_DEBUG, self::LOG_INFO, self::LOG_WARNING, self::LOG_ERROR];
        $currentIndex = array_search($level, $levels);
        $minIndex = array_search($this->minLevel, $levels);
        return $currentIndex >= $minIndex;
    }
}
?>

推荐的日志方案选择:

  • 简单项目:使用文件日志,简单易用
  • 中大型项目:使用数据库日志,便于查询分析
  • 框架项目:使用框架自带的日志功能或中间件
  • 高并发项目:使用异步日志(Redis队列 + 批量写入)

实现要点:

  1. 记录关键信息:用户、模块、操作、时间、IP等
  2. 敏感信息过滤
  3. 日志分级和分类
  4. 定期清理和历史数据处理
  5. 日志查询和统计功能

抱歉,评论功能暂时关闭!