操作日誌是系統審計、問題追溯、合規檢查的核心基礎設施。本文基於 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='操作審計日誌';
<?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',
];
}
<?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;
}
}
<?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('刪除失敗');
}
}
}
logs_202401),配合合併視圖查詢,控制單表體積。$params 處理階段,對 password、id_card、bank_card 等字段進行掩碼或移除。本方案通過獨立連接隔離主業務資料庫故障風險,採用 PDO 預處理防注入,靜態變量複用連接減少開銷,並內建脫敏與 IP 解析邏輯,可直接應用於中大型 ThinkPHP 專案。隨著業務增長,配合異步化與分表策略即可平滑擴展。