首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

ThinkPHP 專案實戰:構建高可用操作日誌系統

     編輯:PHP 綜合知識

前言

操作日誌是系統審計、問題追溯、合規檢查的核心基礎設施。本文基於 ThinkPHP 3.x/5.x 通用架構,展示一套採用獨立資料庫連接、靜態變量複用連接、上下文自動捕獲的日誌實現方案。

一、資料表設計

CREATE TABLE `logs` (
  `id`          INT(11)      NOT NULL AUTO_INCREMENT,
  `guid`        VARCHAR(100) NOT NULL COMMENT '操作者唯一標識',
  `addtime`     TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `account`     VARCHAR(100) NOT NULL COMMENT '登入帳號',
  `nickname`    VARCHAR(100) NOT NULL COMMENT '顯示暱稱',
  `app`         VARCHAR(100) NOT NULL COMMENT '應用/模組名',
  `action`      VARCHAR(100) NOT NULL COMMENT '控制器名',
  `method`      VARCHAR(100) NOT NULL COMMENT '方法名',
  `query_string` TEXT        NOT NULL COMMENT '合併 $_GET/$_POST 參數',
  `is_desc`     TINYINT(1)   NOT NULL DEFAULT 0 COMMENT '是否有詳細描述',
  `desc`        VARCHAR(255) NOT NULL DEFAULT '' COMMENT '操作描述',
  `ipaddr`      VARCHAR(45)  NOT NULL COMMENT '客戶端 IP (支援 IPv6)',
  `unique_flag` VARCHAR(100) NOT NULL DEFAULT 'system' COMMENT '業務標識',
  PRIMARY KEY (`id`),
  KEY `idx_guid_time` (`guid`, `addtime`),
  KEY `idx_flag_time` (`unique_flag`, `addtime`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作審計日誌';

二、核心實現

1. 獨立資料庫配置獲取

<?php
// Application/Common/Common/function.php
function getLogDbConfig(): array {
    return [
        'type'     => C('DB_TYPE'),
        'username' => C('DB_USER'),
        'password' => C('DB_PWD'),
        'host'     => C('DB_HOST'),
        'database' => C('DB_NAME'),
        'charset'  => 'utf8mb4',
    ];
}

2. 日誌寫入核心函數(單例連接 + 預處理語句)

<?php
function addOperationLog(
    string $desc = '',
    string $uniqueFlag = 'system',
    string $app = APP_NAME,
    string $action = MODULE_NAME,
    string $method = ACTION_NAME
): bool {
    static $pdo = null;
    
    // 惰性初始化 PDO 連接(單例模式)
    if ($pdo === null) {
        $cfg = getLogDbConfig();
        $dsn = "mysql:host={$cfg['host']};dbname={$cfg['database']};charset={$cfg['charset']}";
        try {
            $pdo = new PDO($dsn, $cfg['username'], $cfg['password'], [
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES   => false, // 使用真實預處理
            ]);
        } catch (PDOException $e) {
            // 記錄到系統錯誤日誌,避免遞歸調用 addOperationLog
            error_log('[LogDB] Connection failed: ' . $e->getMessage());
            return false;
        }
    }

    // 收集上下文資訊
    $adminId   = session(C('USER_AUTH_KEY')) ?? 0;
    $account   = session('loginAccount') ?? 'unknown';
    $nickname  = session('loginUserName') ?? '';
    $ip        = get_client_ip(1); // 獲取真實 IP
    $params    = array_merge($_GET, $_POST);
    unset($params['password'], $params['repassword']); // 脫敏
    $queryStr  = json_encode($params, JSON_UNESCAPED_UNICODE);
    $hasDesc   = $desc !== '' ? 1 : 0;
    $insertTime = date('Y-m-d H:i:s');

    $sql = "INSERT INTO `" . C('LOG_DB_TABLE') . "` 
            (`guid`,`account`,`nickname`,`addtime`,`app`,`action`,`method`,
             `query_string`,`is_desc`,`desc`,`ipaddr`,`unique_flag`)
            VALUES (?,?,?,?,?,?,?,?,?,?,?,?)";

    try {
        $stmt = $pdo->prepare($sql);
        return $stmt->execute([
            $adminId, $account, $nickname, $insertTime,
            $app, $action, $method, $queryStr,
            $hasDesc, $desc, $ip, $uniqueFlag
        ]);
    } catch (PDOException $e) {
        error_log('[LogDB] Insert failed: ' . $e->getMessage());
        return false;
    }
}

3. 輔助函數:安全獲取客戶端 IP

<?php
function get_client_ip(int $type = 0): string {
    $headers = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 
                'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'];
    foreach ($headers as $header) {
        if (!empty($_SERVER[$header])) {
            $ips = explode(',', $_SERVER[$header]);
            $ip  = trim($ips[0]);
            if (filter_var($ip, FILTER_VALIDATE_IP)) {
                return $type ? ip2long($ip) : $ip;
            }
        }
    }
    return $type ? 0 : '0.0.0.0';
}

三、業務層調用示例

<?php
// Application/Admin/Controller/UserController.class.php
class UserController extends Controller {
    public function delete() {
        $id = I('id', 0, 'intval');
        if ($id <= 0) { $this->error('參數錯誤'); }
        
        $user = D('User')->find($id);
        if (!$user) { $this->error('用戶不存在'); }
        
        $result = D('User')->delete($id);
        if ($result !== false) {
            // 記錄刪除操作,unique_flag 設為 'user_delete' 便於篩選
            addOperationLog(
                "刪除用戶 ID:{$id} 帳號:{$user['account']}",
                'user_delete'
            );
            $this->success('刪除成功');
        } else {
            $this->error('刪除失敗');
        }
    }
}

四、進階優化建議

  1. 異步寫入:高併發場景下,將日誌推入 Redis List / Kafka,由消費者異步入庫,降低主業務延遲。
  2. 分表策略:按月分表(logs_202401),配合合併視圖查詢,控制單表體積。
  3. 敏感字段脫敏:在 $params 處理階段,對 password、id_card、bank_card 等字段進行掩碼或移除。
  4. 查詢介面:構建後台管理介面,支援多條件組合篩選、匯出 CSV、操作趨勢圖表。

五、總結

本方案通過獨立連接隔離主業務資料庫故障風險,採用 PDO 預處理防注入,靜態變量複用連接減少開銷,並內建脫敏與 IP 解析邏輯,可直接應用於中大型 ThinkPHP 專案。隨著業務增長,配合異步化與分表策略即可平滑擴展。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved