ThinkPHP 8模型事件实战:用观察者写了个操作日志功能,真省心

2026-08-18 0 531

公司后台有个需求:所有对订单表的增删改,都要自动记录一条操作日志,写清楚谁在什么时间改了什么字段、旧值是什么、新值是什么。以前的做法是在每个控制器里手动调日志方法,结果一个订单模块十几个接口,漏记了不说,代码还到处重复。后来看到ThinkPHP8的模型事件,发现直接用模型事件去做这个事,一次定义,自动生效,总算把这个烂摊子收拾干净了。

模型事件好在哪里

ThinkPHP的模型在写入、更新、删除后都会触发一些事件,比如after_insertafter_updateafter_delete。你只需要在模型类里定义好对应的方法,这些操作发生时就会自动调用,不需要动控制器代码。如果项目里有多处入口都能改订单,比如后台、接口、定时任务,模型事件就能无一遗漏地记录日志。这比在控制器里手动记录靠谱得多。

我用的ThinkPHP版本

项目用的是ThinkPHP 8.0,PHP 8.2。模型事件在6.0、7.0里也有,但写法上略有不同。这篇文章以TP8为准,如果你用的老版本,大概也能参考。

第一步:创建订单模型和日志模型

订单模型放在app/model/Order.php,日志模型放在app/model/OperateLog.php

Order模型

<?php
namespace appmodel;

use thinkModel;

class Order extends Model
{
    protected $name = 'orders';

    // 这里暂时不写事件方法,回头用观察者来绑定
}

OperateLog模型

<?php
namespace appmodel;

use thinkModel;

class OperateLog extends Model
{
    protected $name = 'operate_logs';

    protected $autoWriteTimestamp = true;

    // 定义日志类型常量
    const TYPE_INSERT = 'insert';
    const TYPE_UPDATE = 'update';
    const TYPE_DELETE = 'delete';
}

第二步:用观察者类集中管理事件

TP8支持把一个类注册为某个模型的观察者。在观察者类里,你可以定义afterInsertafterUpdateafterDelete等方法,这些方法名对应模型事件。我建了个app/observer/OrderObserver.php

<?php
namespace appobserver;

use appmodelOrder;
use appmodelOperateLog;
use thinkfacadeSession;

class OrderObserver
{
    // 新增后
    public function afterInsert(Order $order)
    {
        $this->log($order, OperateLog::TYPE_INSERT);
    }

    // 更新后
    public function afterUpdate(Order $order)
    {
        $this->log($order, OperateLog::TYPE_UPDATE);
    }

    // 删除后(注意删除分为软删除和真实删除,这里只处理真实删除)
    public function afterDelete(Order $order)
    {
        $this->log($order, OperateLog::TYPE_DELETE);
    }

    private function log(Order $order, string $type)
    {
        // 获取当前操作用户,假设在登录时写入了session
        $userId = Session::get('admin_id') ?? 0;
        $username = Session::get('admin_name') ?? 'system';

        // 记录原始数据和修改后的数据
        $originalData = $order->getData();
        // 对于update,可以通过getOrigin获取更新前的数据
        $updateData = $order->getData();

        // 组装差异字段
        $changes = $this->diffFields($order, $type);

        OperateLog::create([
            'order_id'      => $order->id ?? 0,
            'type'          => $type,
            'operator_id'   => $userId,
            'operator_name' => $username,
            'original_json' => $type === OperateLog::TYPE_UPDATE ? json_encode($order->getOrigin(), JSON_UNESCAPED_UNICODE) : json_encode($originalData, JSON_UNESCAPED_UNICODE),
            'changed_json'  => json_encode($updateData, JSON_UNESCAPED_UNICODE),
            'changes'       => json_encode($changes, JSON_UNESCAPED_UNICODE),
            'log_time'      => time(),
        ]);
    }

    private function diffFields(Order $order, string $type)
    {
        if ($type === OperateLog::TYPE_UPDATE) {
            $original = $order->getOrigin();
            $current = $order->getData();
            $diff = [];
            foreach ($current as $field => $value) {
                if (array_key_exists($field, $original) && $original[$field] !== $value) {
                    $diff[$field] = ['old' => $original[$field], 'new' => $value];
                }
            }
            return $diff;
        }

        if ($type === OperateLog::TYPE_INSERT) {
            return $order->getData();
        }

        return [];
    }
}

这里用到了getOrigin(),它返回更新前原始的字段值,用于对比差异。比如用户把订单金额从100改成200,那么差异数组里会记录旧值和新值。

第三步:把观察者注册到模型

在Order模型里增加一行代码,指定观察者类。

<?php
namespace appmodel;

use thinkModel;
use appobserverOrderObserver;

class Order extends Model
{
    protected $name = 'orders';

    // 注册观察者
    protected static function onAfterInit()
    {
        static::observe(OrderObserver::class);
    }
}

也可以把注册放在服务提供者里,但对于单模块项目,直接写在模型里最方便。TP8里模型初始化事件onAfterInit会在模型实例化后触发,在里面调用observe(),观察者就生效了。

第四步:处理软删除的情况

订单表通常使用软删除,也就是数据库里留一个delete_time字段。TP8默认的软删除会走update事件而不是delete事件,所以afterDelete并不会触发。这时候需要监听软删除的onBeforeDelete?其实TP8中软删除是实例调用了delete()方法然后更新数据。因此会触发更新事件,并且where条件里带上delete_time为空。这样上述观察者会把软删除记录成一次字段更新(update),而不是删除日志。如果业务上需要区分软删除,可以改成监听onBeforeDelete,但此时模型里的delete_time还没写入,你无法获取最终修改后的值。另一种做法是在更新事件里检测delete_time字段是否存在并且非空,且原值为null,那就可以判断为软删除。

我把代码调整一下,让它在update事件里判断一下是不是软删除:

public function afterUpdate(Order $order)
{
    $original = $order->getOrigin();
    $current = $order->getData();

    // 如果delete_time从null变成了时间戳,说明是软删除
    if (isset($current['delete_time']) && $current['delete_time'] !== null 
        && (array_key_exists('delete_time', $original) && $original['delete_time'] === null)) {
        $this->log($order, OperateLog::TYPE_DELETE);
    } else {
        $this->log($order, OperateLog::TYPE_UPDATE);
    }
}

这样日志里软删除也会被记成删除类型,比较符合直觉。

第五步:测试效果

我在控制器里写了一个测试方法,模拟管理员修改订单价格:

<?php
namespace appcontroller;

use appmodelOrder;

class Admin
{
    public function test()
    {
        // 模拟登录 - 实际项目中由登录中间件写入
        session([
            'admin_id' => 1,
            'admin_name' => '管理员老王'
        ]);

        $order = Order::find(12);
        $order->price = 200.00;
        $order->save();
    }
}

执行后查看operate_logs表,插入了一条记录,字段大致如下:

order_id: 12
type: update
operator_id: 1
operator_name: 管理员老王
original_json: {"id":12,"order_no":"202412010001","price":100,"status":1,"delete_time":null}
changed_json: {"id":12,"order_no":"202412010001","price":200,"status":1,"delete_time":null}
changes: {"price":{"old":100,"new":200}}

确实能精确到哪个字段改变了,清清楚楚。

在项目里可能遇到的小坑

  1. 模型事件里不能再用模型做查询:在观察者方法里,如果又用Order::where()查询,可能导致事件循环或死锁。因此最好只用当前传入的模型实例。
  2. 批量更新不会触发模型事件:比如Order::where('status', 1)->update(['status' => 2]),这种查询构造器的方式不走模型事件,只有用模型对象调用save和delete才会触发。如果需要记录批量操作,得自己在业务代码里处理。
  3. 保存日志时注意外键约束:如果orders表有物理外键,而删除日志时订单还没被删(afterDelete是在数据库删除之后触发),对于软删除没影响,硬删除的话订单就不存在了,日志里存一个order_id用于溯源即可。
  4. 修改user_id和ip之类的元信息:可以在log方法里加上request的信息,记录操作IP。这就不多写了。

把日志表建一下

如果你需要直接跑这段代码,可以复制下面的SQL建表:

CREATE TABLE `operate_logs` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `order_id` int(11) NOT NULL DEFAULT '0',
  `type` varchar(20) NOT NULL DEFAULT '',
  `operator_id` int(11) NOT NULL DEFAULT '0',
  `operator_name` varchar(50) NOT NULL DEFAULT '',
  `original_json` text,
  `changed_json` text,
  `changes` text,
  `log_time` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `idx_order` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

再扩展一点:全局操作日志

除了订单,其他表如果也要记录日志,可以写一个BaseObserver,让不同模型复用。但不同模型的主要字段不同,模板可能需要做成配置方式。这里的订单日志是写死的,对于快速开发足够。

也可以用事件监听方式绑定模型,不需要观察者,直接在事件定义里写闭包。但观察者类更清晰,方便维护。

总结

模型事件最佳的应用场景就是这种“无侵入的横切逻辑”。如果你还在被一堆打日志的代码纠缠,可以试试这个方案。它不需要修改任何控制器,只需要在模型定义好观察者,后面新增接口也好、定时任务也好,所有走模型的操作全部自动入日志。这个功能给我的最大感受是:ThinkPHP 8学起来还是值。

ThinkPHP 8模型事件实战:用观察者写了个操作日志功能,真省心
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 thinkphp ThinkPHP 8模型事件实战:用观察者写了个操作日志功能,真省心 https://www.taomawang.com/server/thinkphp/2566.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务