内容审核应放在 saved 钩子中并异步 dispatch 队列任务,因此时数据已落库、id 可用、可确保一致性;creating 和 saving 钩子易导致超时、事务卡住或回滚风险。

内容审核该放在模型的哪个钩子
内容审核不能放在 creating 或 saving 钩子里做同步阻塞调用——尤其是对接第三方审核 API(如腾讯云、阿里云)时,网络延迟和限流会导致请求超时或事务卡住。更稳妥的做法是把审核逻辑下沉到 saved 钩子,并立即 dispatch 一个队列任务。
原因很直接:模型保存成功后才触发审核,避免因审核失败回滚业务数据;同时队列能自动重试、隔离失败影响、支持异步超时控制。
-
creating:数据还没入库,审核失败会导致用户感知“提交失败”,但其实没留任何记录,不利于排查 -
saving:仍处于事务中,http 请求可能被中断,且 laravel 默认事务不跨队列,无法保证一致性 -
saved:已落库、ID 可用、可安全 dispatch,是唯一推荐的入口点
如何在 saved 钩子里安全 dispatch 审核任务
直接在模型里写 dispatch(new ReviewContentJob($this)) 看似简单,但有隐患:模型属性可能被后续操作修改(比如事件监听器),或者模型使用了 toArray() 序列化时丢失关联关系。
正确做法是只传关键标识,让 Job 自行查库重建上下文:
protected static function booted() { static::saved(function ($model) { if ($model->wasRecentlyCreated && $model->shouldReview()) { ReviewContentJob::dispatch($model->getMorphClass(), $model->id); } }); }
-
$model->getMorphClass()用于兼容多态模型(如Post、Comment) - Job 构造函数里不做 DB 查询,
handle()中用app()->make($type)::findOrFail($id)确保数据新鲜 - 加
$model->wasRecentlyCreated判断,避免更新时重复审核
审核失败后怎么通知和降级
审核失败不能静默吞掉——既影响运营感知,也阻碍问题定位。要在 Job 的 failed() 方法里明确处理路径:
- 记录失败日志 + 报警(如 Slack / 钉钉 Webhook),带上
$exception->getMessage()和$content->id - 给内容打上
review_status = 'pending'或'failed'标记,方便后台人工介入 - 如果业务允许,可 fallback 到本地关键词过滤(
Str::contains($text, ['敏感词1', '敏感词2'])),并标记为'auto_rejected' - 切勿在失败时软删除或硬删内容——审核只是风控环节,不是终审判决
为什么不要在模型里写审核逻辑本身
把 HTTP 调用、签名生成、响应解析这些细节塞进模型,会快速污染模型职责,导致测试难、复用差、升级痛。
真正该做的只有两件事:判断是否需要审核(shouldReview())、决定由谁审核(getReviewDriver())。其余交给独立 Service:
class ContentReviewService { public function review(string $type, int $id): ReviewResult { $driver = app()->make($this->getDriverConfig($type)); return $driver->send($type, $id); } }
这样换审核服务商时,只需新增一个 Driver 实现,模型和 Job 完全不用动。容易被忽略的是:Driver 必须自己管理连接超时(guzzle timeout=3)、重试次数(max_attempts=2)、以及对 429 响应的退避策略——这些细节模型层根本不该碰。