本文目录导读:

- 方案一:简单的操作日志(记录“谁”在“何时”做了什么)
- 方案二:存储数据快照(支持查看历史版本和回滚)
- 方案三:存储字段级差异(只记录变化的部分)
- 方案四:使用现成库 / 框架扩展
- 方案五:基于事件溯源(Event Sourcing,适用大型/复杂系统)
- 总结:如何选择?
在 PHP 项目中实现版本历史(版本管理/审计日志)通常有几种方案,取决于你的具体需求(如:只记录谁改了、改了什么、还是支持回滚)。
以下是几种主流且实用的实现方式,从简单到复杂:
简单的操作日志(记录“谁”在“何时”做了什么)
这是最基础的需求,只需要记录操作行为,不记录具体数据变化。
实现方式: 使用一个数据库表来存储日志。
表结构示例:
CREATE TABLE `operation_logs` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL COMMENT '操作用户ID',
`action` VARCHAR(50) NOT NULL COMMENT '操作类型:create, update, delete',
`target_type` VARCHAR(100) NOT NULL COMMENT '操作对象类型:Article, User, Order',
`target_id` INT NOT NULL COMMENT '操作对象ID',
`description` VARCHAR(500) COMMENT '操作描述,如:修改了文章标题',
`ip_address` VARCHAR(45) COMMENT '操作IP',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_target` (`target_type`, `target_id`),
INDEX `idx_user` (`user_id`),
INDEX `idx_time` (`created_at`)
);
PHP 记录日志示例:
<?php
class AuditLogger {
public static function log($userId, $action, $targetType, $targetId, $description = '') {
// 使用 PDO 或 ORM 插入数据库
db()->insert('operation_logs', [
'user_id' => $userId,
'action' => $action,
'target_type' => $targetType,
'target_id' => $targetId,
'description' => $description,
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '',
]);
}
}
// 在业务代码中调用
// AuditLogger::log(123, 'update', 'Article', 456, '修改了文章标题和内容');
优点: 实现简单,性能好,适合查看操作记录。 缺点: 无法查看数据修改前后的具体值,无法回滚。
存储数据快照(支持查看历史版本和回滚)
这种方式可以完整记录每次修改的数据状态,常用于 CMS、文档编辑等场景。
实现方式: 为每个需要版本管理的表,创建一个对应的“版本历史表”。
示例:文章表
- 主表(articles): 只存储当前最新数据。
- 版本历史表(article_versions): 存储每次修改的完整数据快照。
表结构:
-- 主表(当前数据)
CREATE TABLE `articles` (
`id` INT PRIMARY KEY AUTO_INCREMENT, VARCHAR(200),
`content` TEXT,
`status` VARCHAR(20),
`updated_by` INT,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`current_version_id` INT DEFAULT NULL COMMENT '指向当前版本ID' -- 可选
);
-- 版本历史表
CREATE TABLE `article_versions` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`article_id` INT NOT NULL,
`version_number` INT NOT NULL COMMENT '版本号,递增', VARCHAR(200),
`content` TEXT,
`status` VARCHAR(20),
`snapshot_data` JSON COMMENT '完整数据快照 (可选,如果字段太多可以存JSON)',
`created_by` INT NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`change_summary` VARCHAR(500) COMMENT '本次修改的摘要',
INDEX `idx_article` (`article_id`)
);
PHP 保存版本(更新文章时):
<?php
class ArticleService {
public function updateArticle($articleId, $data, $userId) {
// 1. 获取当前文章数据(用于记录变更)
$currentArticle = db()->get('articles', $articleId);
// 2. 计算新版本号
$lastVersion = db()->get('article_versions', ['article_id' => $articleId], 'order' => 'version_number DESC');
$newVersionNumber = ($lastVersion ? $lastVersion['version_number'] : 0) + 1;
// 3. 保存当前版本到历史表(保存更新前的数据,或保存新数据)
$versionId = db()->insert('article_versions', [
'article_id' => $articleId,
'version_number' => $newVersionNumber,
'title' => $data['title'] ?? $currentArticle['title'],
'content' => $data['content'] ?? $currentArticle['content'],
'status' => $data['status'] ?? $currentArticle['status'],
'snapshot_data' => json_encode($currentArticle), // 保存旧数据作为快照
'created_by' => $userId,
'change_summary' => $data['change_summary'] ?? '',
]);
// 4. 更新主表
db()->update('articles', array_merge($data, [
'current_version_id' => $versionId,
'updated_by' => $userId,
]), ['id' => $articleId]);
return true;
}
// 回滚到指定版本
public function rollbackToVersion($articleId, $versionId, $userId) {
$version = db()->get('article_versions', $versionId);
if (!$version || $version['article_id'] != $articleId) {
throw new Exception('版本不存在');
}
// 回滚:将主表数据更新为版本数据
db()->update('articles', [
'title' => $version['title'],
'content' => $version['content'],
'status' => $version['status'],
'updated_by' => $userId,
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => $articleId]);
// 可选:记录一个回滚操作到版本历史
// ...
}
}
优点: 完整的数据追溯,支持“时光机”效果,可以回滚。 缺点: 存储空间占用大(每次修改都保存一份完整数据),可通过只保存差异(diff)来优化。
存储字段级差异(只记录变化的部分)
如果数据表字段很多,但每次只改一两个字段,可以在版本历史中只存储修改的差异(Diff),而不是整个对象。
实现方式:
使用 JSON 或 TEXT 字段存储 changed_fields:
ALTER TABLE `article_versions` ADD COLUMN `changed_fields` JSON COMMENT '只记录发生变化的字段及其新值';
PHP 生成差异:
$oldData = ['title' => '旧标题', 'content' => '旧内容', 'status' => 'draft'];
$newData = ['title' => '新标题', 'content' => '旧内容', 'status' => 'published'];
$changed = [];
foreach ($newData as $key => $value) {
if ($oldData[$key] != $value) {
$changed[$key] = ['old' => $oldData[$key], 'new' => $value];
}
}
// $changed = ['title' => ['old' => '旧标题', 'new' => '新标题'], 'status' => [...]]
优点: 大幅减少存储空间,便于展示“到底改了什么”。 缺点: 回滚时需要根据差异反向恢复(逻辑稍复杂),如果需要完整数据快照,可能还需要额外存储。
使用现成库 / 框架扩展
如果不想手写,可以使用成熟的扩展:
-
Laravel 用户:
spatie/laravel-activitylog:强大的操作日志系统。venturecraft/revisionable:自动记录 Eloquent 模型的修改历史。mpociot/versionable:专注于为 Eloquent 模型提供版本管理。
-
Symfony 用户:
gedmo/doctrine-extensions的 Loggable 行为,自动记录实体变更。
-
通用:
数据版本控制层(如 Event Sourcing 模式的简化版)。
基于事件溯源(Event Sourcing,适用大型/复杂系统)
这是最强大的方案,但不是“用 PHP 项目实现”,而是“PHP 项目采用事件驱动架构”。
- 核心思想:不存储当前状态,只存储所有事件(
ArticleCreated,TitleChanged,ContentUpdated)。 - 当前状态 = 所有事件按顺序重放的最终结果。
- 版本历史 = 事件流本身。
优点: 完美的审计、可完全重建历史、支持复杂回滚。 缺点: 学习曲线陡峭,查询当前状态性能有开销(需要聚合),不适合简单项目。
如何选择?
| 需求 | 推荐方案 |
|---|---|
| 只记录“谁在何时做了什么事”(如后台操作日志) | 简单操作日志 |
| 需要查看文章、商品等数据的历史版本,并支持回滚 | 数据快照 |
| 字段很多,但每次只改少数字段,希望节省空间 | 字段级差异 |
| 使用 Laravel/Symfony 框架,希望开箱即用 | 现成库 |
| 项目规模大,需要高度可审计性和事件流 | 事件溯源 |
建议: 绝大多数 PHP 项目(CMS、电商、ERP)选择 方案二 + 方案三的结合 即可——存储字段级差异,同时定期提供完整快照(比如每10个版本保存一次完整快照),这样兼顾了存储效率和回滚能力。