必须用migration打底并配合模型事件实现字段变更留痕:先安装think-migration扩展,创建migration元表和field_history审计表,再通过模型after_update事件捕获变更写入快照,迁移文件需规范命名与编写,确保回滚不影响历史记录。

想在ThinkPHP项目中实现数据库结构变更可追溯、字段修改留痕、回滚不丢数据,又不想自己从零写审计日志表或硬编码版本比对逻辑——必须用Migration打底,再配合模型事件补字段快照。
初始化迁移系统并创建首张history表
先执行composer require topthink/think-migration:^4.0安装TP6/TP7兼容的最新版扩展,旧版v2.x不支持PHP 8.2+且无字段变更钩子能力。
运行php think migrate:install创建think_migration元数据表——这步跳过会导致后续所有命令报“Table 'think_migration' doesn't exist”,不是错觉,是真没建表。
手动创建database/migrations/20260602000000_create_field_history_table.php文件,类名必须为CreateFieldHistoryTable,继承think\migration\Migrator;【up()中必须用$this->table()->create()而非原生SQL,否则down()无法自动推断删除逻辑】
在up()方法里定义history表结构:含table_name、field_name、old_value、new_value、operator、created_at字段,其中old_value和new_value用text类型,避免JSON字段被截断。
给关键模型添加字段变更监听
在app/model/User.php顶部添加use think\model\Event;,并在initialize()方法末尾注册监听:Event::listen('model.after_update', [$this, 'onFieldChange']);
编写onFieldChange()方法:遍历$this->getChangedData()返回的键值对,对每个变更字段构造一条history记录,调用Db::name('field_history')->insert()写入——注意这里不能用UserModel::create(),迁移阶段模型未加载,但运行时可用Db门面。
这一步不做,字段改了就石沉大海;做了但没清空runtime/cache/,可能因配置缓存导致监听不触发。
生成带时间戳的字段变更迁移文件
第一步:运行php think migrate:create AddStatusToOrder,生成20260602125301_add_status_to_order.php文件。
第二步:打开该文件,在up()中调用$this->table('order')->addColumn('status', 'string', ['limit' => 20, 'default' => 'pending']);【down()必须手写$this->table('order')->dropColumn('status'),框架绝不会帮你猜】
第三步:保存后立即执行php think migrate:run。若提示“No migrations to run”,立刻检查文件是否在database/migrations/下、文件名有无Windows隐藏空格、类名是否与_后部分完全一致。
验证字段快照是否生效
方法一:直接更新用户数据,如UserModel::where('id', 1)->update(['mobile' => '13800138000']),然后查field_history表,确认新增记录的old_value是空或旧值、new_value是新手机号、operator为当前登录用户名(需自行注入)。
方法二:执行php think migrate:rollback --step=1回退上一个迁移,观察order表中status字段是否消失,同时确认field_history表不受影响——它只记录业务变更,不参与结构迁移。
方法三:在测试环境执行php think migrate:status,核对up列是否全为Y,若有N说明该迁移文件未被执行,别急着重跑,先看think_migration表里version字段最大值是否小于该文件时间戳。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











