本文目录导读:

在PHP项目中实现用户自定义回滚功能,通常需要结合数据库事务、操作日志记录和状态快照,以下是几种常见的实现方案:
基于数据库事务的回滚
核心思路
将每次用户操作包装在事务中,记录操作步骤,允许用户撤销最近的N步操作。
<?php
class TransactionRollback {
private $db;
private $operationStack = [];
public function __construct($pdo) {
$this->db = $pdo;
}
// 开始一个可回滚的操作
public function beginOperation($operationName) {
$this->db->beginTransaction();
// 保存当前状态快照(用于回滚)
$snapshot = $this->captureSnapshot();
$operation = [
'name' => $operationName,
'snapshot' => $snapshot,
'time' => time()
];
array_push($this->operationStack, $operation);
}
// 提交当前操作
public function commitOperation() {
$this->db->commit();
}
// 回滚到指定步骤
public function rollbackTo($steps = 1) {
if (count($this->operationStack) < $steps) {
throw new Exception("没有足够的操作可以回滚");
}
// 获取要回滚到的快照
$targetOperation = $this->operationStack[count($this->operationStack) - $steps];
$snapshot = $targetOperation['snapshot'];
// 回滚到快照状态
$this->restoreSnapshot($snapshot);
// 移除已回滚的操作
array_splice($this->operationStack, count($this->operationStack) - $steps);
}
// 捕获当前数据库状态快照
private function captureSnapshot() {
// 示例:记录受影响表的主键和关键字段
$tables = ['users', 'orders'];
$snapshot = [];
foreach ($tables as $table) {
$stmt = $this->db->query("SELECT * FROM {$table} ORDER BY id");
$snapshot[$table] = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
return $snapshot;
}
// 恢复数据库到快照状态
private function restoreSnapshot($snapshot) {
foreach ($snapshot as $table => $rows) {
// 清空表
$this->db->exec("TRUNCATE TABLE {$table}");
// 重新插入数据
foreach ($rows as $row) {
$columns = implode(', ', array_keys($row));
$placeholders = ':' . implode(', :', array_keys($row));
$stmt = $this->db->prepare("INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})");
$stmt->execute($row);
}
}
}
}
// 使用示例
$rollback = new TransactionRollback($pdo);
try {
$rollback->beginOperation('updateUser');
// 执行用户操作
$pdo->exec("UPDATE users SET balance = balance - 100 WHERE id = 1");
$pdo->exec("UPDATE orders SET status = 'paid' WHERE user_id = 1");
$rollback->commitOperation();
} catch (Exception $e) {
$rollback->rollbackTo(1); // 回滚最近一步操作
}
基于日志的快照回滚
核心思路
记录每个操作的详细日志,回滚时逆向执行这些操作。
<?php
class LogBasedRollback {
private $db;
private $logTable = 'operation_logs';
private $maxHistory = 50; // 最多保存50步操作历史
public function __construct($pdo) {
$this->db = $pdo;
$this->initLogTable();
}
// 初始化日志表
private function initLogTable() {
$sql = "
CREATE TABLE IF NOT EXISTS {$this->logTable} (
id INT AUTO_INCREMENT PRIMARY KEY,
operation_name VARCHAR(100),
entity_type VARCHAR(50),
entity_id INT,
old_value JSON,
new_value JSON,
user_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_time (user_id, created_at)
)
";
$this->db->exec($sql);
}
// 记录操作
public function logOperation($operationName, $entityType, $entityId, $oldValue, $newValue, $userId) {
$stmt = $this->db->prepare(
"INSERT INTO {$this->logTable}
(operation_name, entity_type, entity_id, old_value, new_value, user_id)
VALUES (?, ?, ?, ?, ?, ?)"
);
$stmt->execute([
$operationName,
$entityType,
$entityId,
json_encode($oldValue),
json_encode($newValue),
$userId
]);
// 限制历史记录数量
$this->cleanupOldLogs($userId);
}
// 执行回滚
public function rollback($userId, $steps = 1) {
// 获取最后N条操作记录
$stmt = $this->db->prepare(
"SELECT * FROM {$this->logTable}
WHERE user_id = ?
ORDER BY id DESC
LIMIT ?"
);
$stmt->execute([$userId, $steps]);
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 逆向执行回滚
foreach ($logs as $log) {
$this->performRollback($log);
// 删除已回滚的日志
$this->db->exec("DELETE FROM {$this->logTable} WHERE id = {$log['id']}");
}
return count($logs);
}
// 执行单个回滚操作
private function performRollback($log) {
$entityType = $log['entity_type'];
$entityId = $log['entity_id'];
$oldValue = json_decode($log['old_value'], true);
switch ($entityType) {
case 'user':
$this->rollbackUser($entityId, $oldValue);
break;
case 'order':
$this->rollbackOrder($entityId, $oldValue);
break;
// 添加更多实体类型
}
}
private function rollbackUser($userId, $oldValue) {
$setClause = [];
foreach ($oldValue as $field => $value) {
$setClause[] = "{$field} = :{$field}";
}
$sql = "UPDATE users SET " . implode(', ', $setClause) . " WHERE id = :id";
$stmt = $this->db->prepare($sql);
$stmt->execute(array_merge($oldValue, ['id' => $userId]));
}
private function rollbackOrder($orderId, $oldValue) {
// 类似上面的逻辑
}
// 清理旧日志
private function cleanupOldLogs($userId) {
$stmt = $this->db->prepare(
"DELETE FROM {$this->logTable}
WHERE user_id = ? AND id NOT IN (
SELECT id FROM (
SELECT id FROM {$this->logTable}
WHERE user_id = ?
ORDER BY id DESC
LIMIT {$this->maxHistory}
) AS temp
)"
);
$stmt->execute([$userId, $userId]);
}
}
// 使用示例
$rollback = new LogBasedRollback($pdo);
// 更新用户信息
$oldUser = $pdo->query("SELECT * FROM users WHERE id = 1")->fetch(PDO::FETCH_ASSOC);
$newUser = $oldUser;
$newUser['balance'] = 1000;
// 执行更新
$pdo->exec("UPDATE users SET balance = 1000 WHERE id = 1");
// 记录操作
$rollback->logOperation('updateUser', 'user', 1, $oldUser, $newUser, $userId);
// 用户点击"撤销"
$rollback->rollback($userId, 1);
更高级的版本控制回滚
核心思路
类似Git的版本控制,每次操作创建完整的数据快照。
<?php
class VersionControlRollback {
private $db;
private $versionTable = 'data_versions';
public function __construct($pdo) {
$this->db = $pdo;
$this->initVersionTable();
}
private function initVersionTable() {
$sql = "
CREATE TABLE IF NOT EXISTS {$this->versionTable} (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
version_number INT,
snapshot_data LONGBLOB,
operation_name VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_version (user_id, version_number)
)
";
$this->db->exec($sql);
}
// 创建新版本
public function createVersion($userId, $operationName) {
// 获取当前所有相关数据
$snapshot = $this->captureFullSnapshot();
// 获取当前用户的最新版本号
$stmt = $this->db->prepare(
"SELECT MAX(version_number) as max_version
FROM {$this->versionTable}
WHERE user_id = ?"
);
$stmt->execute([$userId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$newVersion = ($result['max_version'] ?? 0) + 1;
// 保存版本
$stmt = $this->db->prepare(
"INSERT INTO {$this->versionTable}
(user_id, version_number, snapshot_data, operation_name)
VALUES (?, ?, ?, ?)"
);
$stmt->execute([
$userId,
$newVersion,
gzcompress(serialize($snapshot)),
$operationName
]);
return $newVersion;
}
// 回滚到指定版本
public function rollbackToVersion($userId, $targetVersion) {
// 获取目标版本快照
$stmt = $this->db->prepare(
"SELECT * FROM {$this->versionTable}
WHERE user_id = ? AND version_number = ?"
);
$stmt->execute([$userId, $targetVersion]);
$version = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$version) {
throw new Exception("版本不存在");
}
// 解压并恢复快照
$snapshot = unserialize(gzuncompress($version['snapshot_data']));
$this->restoreFullSnapshot($snapshot);
// 删除该版本之后的所有版本
$stmt = $this->db->prepare(
"DELETE FROM {$this->versionTable}
WHERE user_id = ? AND version_number > ?"
);
$stmt->execute([$userId, $targetVersion]);
return true;
}
// 获取版本历史
public function getVersionHistory($userId) {
$stmt = $this->db->prepare(
"SELECT id, version_number, operation_name, created_at
FROM {$this->versionTable}
WHERE user_id = ?
ORDER BY version_number DESC
LIMIT 20"
);
$stmt->execute([$userId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
private function captureFullSnapshot() {
$snapshot = [];
// 捕获所有需要版本控制的表
$tables = ['users', 'orders', 'products', 'categories'];
foreach ($tables as $table) {
$stmt = $this->db->query("SELECT * FROM {$table} ORDER BY id");
$snapshot[$table] = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
return $snapshot;
}
private function restoreFullSnapshot($snapshot) {
$this->db->beginTransaction();
try {
foreach ($snapshot as $table => $rows) {
$this->db->exec("DELETE FROM {$table}");
if (!empty($rows)) {
$stmt = $this->prepareInsertStatement($table, $rows[0]);
foreach ($rows as $row) {
$stmt->execute($row);
}
}
}
$this->db->commit();
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
private function prepareInsertStatement($table, $row) {
$columns = array_keys($row);
$placeholders = ':' . implode(', :', $columns);
$columns = implode(', ', $columns);
return $this->db->prepare("INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})");
}
}
// 前端展示版本历史示例
function renderVersionTimeline($versions) {
echo '<div class="version-timeline">';
foreach ($versions as $version) {
echo '<div class="version-item">';
echo "v{$version['version_number']}: {$version['operation_name']} ";
echo "({$version['created_at']})";
echo '<button onclick="rollbackTo(' . $version['version_number'] . ')">回滚到这里</button>';
echo '</div>';
}
echo '</div>';
}
前端交互实现
// rollback.js
class RollbackManager {
constructor(options) {
this.apiEndpoint = options.apiEndpoint || '/api/rollback';
this.userId = options.userId;
this.maxHistory = options.maxHistory || 50;
}
// 获取操作历史
async getHistory(page = 1) {
const response = await fetch(`${this.apiEndpoint}/history?user_id=${this.userId}&page=${page}`);
return response.json();
}
// 执行回滚
async performRollback(steps = 1, type = 'step') {
try {
const response = await fetch(`${this.apiEndpoint}/execute`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: this.userId,
steps: steps,
type: type
})
});
const result = await response.json();
if (result.success) {
this.showSuccess('回滚成功');
return true;
} else {
this.showError(result.message);
return false;
}
} catch (error) {
this.showError('回滚失败: ' + error.message);
return false;
}
}
// 显示操作历史UI
renderHistory(history) {
const container = document.getElementById('rollback-history');
container.innerHTML = '';
history.forEach((item, index) => {
const element = document.createElement('div');
element.className = 'history-item';
element.innerHTML = `
<span class="operation-name">${item.operation_name}</span>
<span class="operation-time">${new Date(item.created_at).toLocaleString()}</span>
<button class="rollback-btn" data-steps="${index + 1}">
撤销到这一步
</button>
`;
element.querySelector('.rollback-btn').addEventListener('click', () => {
this.performRollback(index + 1);
});
container.appendChild(element);
});
}
showSuccess(message) {
// 显示成功提示
}
showError(message) {
// 显示错误提示
}
}
// 使用示例
const rollbackManager = new RollbackManager({
apiEndpoint: '/api/rollback',
userId: 123
});
// 加载历史记录
rollbackManager.getHistory().then(history => {
rollbackManager.renderHistory(history);
});
API接口示例
<?php
// rollback_api.php
class RollbackAPI {
private $rollbackService;
public function __construct($pdo) {
$this->rollbackService = new VersionControlRollback($pdo);
}
public function handleRequest() {
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
switch ($method) {
case 'GET':
if ($action === 'history') {
$this->getHistory();
}
break;
case 'POST':
if ($action === 'execute') {
$this->executeRollback();
}
break;
}
}
private function getHistory() {
$userId = $_GET['user_id'] ?? 0;
if (!$userId) {
$this->error('User ID required');
}
$history = $this->rollbackService->getVersionHistory($userId);
$this->success(['history' => $history]);
}
private function executeRollback() {
$input = json_decode(file_get_contents('php://input'), true);
$userId = $input['user_id'] ?? 0;
$steps = $input['steps'] ?? 1;
$type = $input['type'] ?? 'step';
if (!$userId) {
$this->error('User ID required');
}
try {
if ($type === 'step') {
// 基于步骤回滚
$result = $this->rollbackService->rollbackToVersion($userId, $steps);
} else if ($type === 'version') {
// 基于版本回滚
$result = $this->rollbackService->rollbackToVersion($userId, $steps);
}
$this->success(['message' => 'Rollback successful']);
} catch (Exception $e) {
$this->error($e->getMessage());
}
}
private function success($data) {
header('Content-Type: application/json');
echo json_encode(['success' => true, 'data' => $data]);
exit;
}
private function error($message) {
header('HTTP/1.1 400 Bad Request');
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => $message]);
exit;
}
}
// 路由示例
$api = new RollbackAPI($pdo);
$api->handleRequest();
最佳实践建议
-
性能优化:
- 使用增量快照而不是全量快照
- 定期清理过期历史记录
- 使用异步任务处理大规模回滚
-
安全性考虑:
- 添加用户权限验证
- 限制可回滚的步数
- 记录所有回滚操作
-
用户体验:
- 提供预览功能
- 显示回滚影响范围
- 支持批量选择回滚
-
数据完整性:
- 使用事务确保原子性
- 提供回滚确认机制
- 保存原始数据备份
选择哪种实现方案取决于你的具体需求:
- 简单场景:使用事务回滚
- 中等复杂:使用日志回滚
- 复杂场景:使用版本控制回滚
建议在开发环境中充分测试后再部署到生产环境。