Laravel中用mpociot/versionable实现文档版本控制,自动记录字段变更、支持软删除,需发布迁移、配置排除字段与保留上限,并将文件存对象存储后跟踪路径变更。

在 Laravel 应用中实现文件版本控制与历史回溯,不是靠 Git 管理源码,而是让业务数据(如合同、文档、配置文件)本身具备可追溯的存档能力——比如用户上传一份 PDF 合同后,每次编辑都生成新版本并保留旧版,点击任意历史版本即可下载或对比。
选择模型级版本控制方案
直接使用 【mpociot/versionable】 扩展包,它专为 Eloquent 模型设计,自动捕获字段变更、序列化快照、支持软删除兼容,比手写版本表+触发器更可靠且易维护。
执行安装命令:composer require mpociot/versionable。
运行迁移前,先将迁移文件发布到项目目录:php artisan vendor:publish --provider="Mpociot\Versionable\VersionableServiceProvider" --tag=migrations。这一步必须做,否则生产环境无法审计迁移来源。
接着执行:php artisan migrate,生成 versions 表。
配置需要版本化的模型
在目标模型(例如 App\Models\Document)中引入 trait:
use Mpociot\Versionable\VersionableTrait;
并在类体内启用该 trait:
class Document extends Model { use VersionableTrait; }
默认情况下,所有可填充字段都会被记录。若某些字段(如 updated_at、view_count)无需版本留存,必须显式排除:protected $dontVersionFields = ['updated_at', 'view_count'];。漏掉这个会导致每次访问都触发无意义版本快照,快速撑爆数据库。
为防版本膨胀,设置保留上限:protected $keepOldVersions = 20;。该值会在每次保存新版本时自动清理超出数量的旧记录。
存储文件并关联版本快照
不要把文件二进制内容直接塞进 versions 表——Laravel Versionable 默认只序列化模型属性,不处理文件流。
正确做法是:上传文件到对象存储(如 S3 或本地 storage/app/documents),保存路径到模型字段(如 file_path),再让 Versionable 跟踪该字段变化。
示例操作路径:request()->file('document')->store('documents', 'public') → 保存路径到 $document->file_path → $document->save()。
此时 Versionable 会自动记录 file_path 字段变更,并在 versions 表中存下该次快照的完整模型状态(含关联的 user_id、title 等)。
按文档编号查询最新/历史版本
第一步:获取每个 document_number 对应的最新版本号(数值型)
DB::table('versions')
→ select('document_number')
→ selectRaw('MAX(CAST(SUBSTRING_INDEX(version, " ", -1) AS UNSIGNED)) as version_number')
→ where('table_name', 'documents')
→ groupBy('document_number')
→ get();
第二步:用结果集反查具体版本记录
遍历上一步结果,对每个 document_number 和 version_number 构造查询条件,从 versions 表中取出完整快照数据,包括 file_path、title、created_at 等原始字段。
注意:如果 version 字段存的是语义化字符串(如 "v2.1" 或 "Draft-3"),SUBSTRING_INDEX 提取逻辑需同步调整,否则 MAX() 会按字典序误判。
回溯并还原指定历史版本
方法一:仅还原模型字段(不含文件)
从 versions 表查出目标快照的 version_data 字段(JSON 格式),用 json_decode($row->version_data, true) 解析,再用 fill() 写回当前模型实例,最后 save() 即可。
方法二:连同文件一起还原
先还原模型字段,再根据快照中的 file_path 值,从对应存储位置复制一份副本,重命名为带时间戳的新路径(避免覆盖当前文件),最后更新模型的 file_path 指向新副本并保存。
这一步不能跳过重命名,否则多个版本共用同一路径会导致还原后打开的是最新版文件。


















