本节摘要:幕后车间的时间与广播系统:调度器按时刻表唤醒周期任务,一条服务器 crontab 驱动全部 Laravel 计划;模型事件与自定义事件让"状态变了"成为可订阅的消息;通知系统一次声明、邮件与数据库多通道送达。本节把三套机制接进工单系统,读完你能搭出"每晚巡检、状态联动、完工即通知"的自动化闭环。
上一节的队列解决了"慢活后台干",但还有两类需求没着落。一是时间维度:过期工单每晚自动归档、日报每天早上八点生成——这类活没人提交,得到点自己醒。二是联动维度:工单一竣工,队组要收站内信、管理员要收邮件、日志表要留痕——这些"跟随状态变化"的动作不该写死在控制器里,否则每加一个联动点就要改一遍业务代码。调度器管第一类,事件与通知管第二类。
所有计划任务写在 routes/console.php(Laravel 11 之前在 app/Console/Kernel.php 的 schedule 方法):
<?php // routes/console.php use Illuminate\Support\Facades\Schedule; // 每天凌晨两点归档过期工单 Schedule::command('orders:archive-stale')->dailyAt('02:00'); // 工作日早上八点发当日待办日报 Schedule::job(new DailyReportJob)->weekdays()->at('08:00'); // 每小时清理一次失败的临时文件 Schedule::call(fn () => Storage::deleteDirectory('tmp/stale')) ->hourly()->onOneServer(); // 手工建闭包任务也一样能挂时刻表 Schedule::call(function () { \App\Models\WorkOrder::where('status', 'done') ->where('updated_at', '<', now()->subDays(30)) ->update(['status' => 'archived']); })->daily();
服务器侧只需要一条系统 crontab,每分钟叫醒一次调度器,由它判断哪条计划到点了:
* * * * * cd /var/www/workorder && php artisan schedule:run >> /dev/null 2>&1
这个设计的好处藏在细节里:时刻表进了版本管理(crontab 只有一条且永不增删);onOneServer 与 withoutOverlapping 防止多机重复执行、防长任务叠跑。排查"定时任务没跑",按顺序查三样:系统 crontab 在不在、schedule:list 列出的计划对不对、任务本身手动执行是否报错。
工单从"施工中"变"已完工"是一个模型状态的迁移,任何关心这个变化的角色(通知、日志、统计)都不该被硬编码在改状态的代码里。事件把它们解耦:
php artisan make:event OrderCompleted php artisan make:listener SendCompletionNotice --event=OrderCompleted
<?php // 事件本体:携带现场数据的数据袋 namespace App\Events; use App\Models\WorkOrder; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; class OrderCompleted { use Dispatchable, InteractsWithSockets, SerializesModels; public function __construct(public WorkOrder $order) {} }
<?php // 监听器:实现 ShouldQueue 即自动走队列(呼应上一节) namespace App\Listeners; use App\Events\OrderCompleted; use Illuminate\Contracts\Queue\ShouldQueue; class SendCompletionNotice implements ShouldQueue { public function handle(OrderCompleted $event): void { $event->order->team->notify( new \App\Notifications\OrderDone($event->order) ); } }
<?php // 触发侧:业务代码只管广播,不关心谁来听 public function complete(Request $request, Order $order) { $this->authorize('update', $order); $order->markAsDone(); // 内部改状态 OrderCompleted::dispatch($order); // 广播出去 return back()->with('status', '已完工'); }
模型级的变化还可以用观察者(Observer)收口:给 Order 建一个 Observer,updated 钩子里判断 status 迁移并派发领域事件,改状态的代码就彻底不写 dispatch 了。事件驱动的边界也要点破:不要把核心流程依赖挂在事件上——监听器是异步的、可失败的,"没这步业务就不成立"的逻辑留在主流程里,事件只做锦上添花的联动。

php artisan make:notification OrderDone
<?php namespace App\Notifications; use App\Models\WorkOrder; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; class OrderDone extends Notification { use Queueable; // 通知也走队列:发送动作不拖住监听器 public function __construct(public WorkOrder $order) {} // 通道清单:一行一个世界 public function via(object $notifiable): array { return ['mail', 'database']; } public function toMail(object $notifiable): MailMessage { return (new MailMessage) ->subject("工单 {$this->order->sn} 已完工") ->greeting('验收提醒') ->line("《{$this->order->title}》已完工,请安排验收。") ->action('去看单子', url("/orders/{$this->order->id}")); } // 数据库通道:存进 notifications 表,站内铃铛读它 public function toArray(object $notifiable): array { return ['order_id' => $this->order->id, 'type' => 'completed']; } } // 发送侧:任何 Notifiable 对象一行调 $team->notify(new OrderDone($order)); // 测试与调试利器:Notification::fake() 断言发了什么(第九章用到)
想加短信、企业微信这类新通道,要么实现自定义 Channel 类(一个 public function send 即一个通道),要么引入社区包。通道的"分人群路由"在 notifiable 侧控制——队组大老板走短信、普通成员走站内,via($notifiable) 按人返回不同清单即可。
💡 关键直觉:事件解决"谁知道这件事",通知解决"怎么送到他手上"。两层分开,加一个新联动只是加一个监听器,加一个新通道只是加一个 toXxx 方法——这就是广播站的价值。
幕后车间全部投产。下一章收官:竣工验收的测试体系、上线前的提速改造,以及把整个工地正式交付的部署流水线。