前言:一个“简单”功能的开始

        前段时间我接手第一个电商项目时,产品经理丢来一个需求:

“商品库存低于阈值时,给商家发个微信提醒,最好再来封邮件。”

        听起来再简单不过。我花半小时写了个 decreaseStock() 方法,里面顺手调了微信接口 —— 测试通过,上线。但接下来的三个月,我至少在这个“简单功能”上改了七八次代码,每次改都心惊胆战。直到后来我接触到事件驱动架构,才恍然大悟:原来一个通知功能,竟然是检验系统设计的绝佳试金石。

        这篇文章不会教你怎么“用 Laravel 实现低库存通知”,因为这样的代码一搜一大把。我想带你走一遍真实的演进之路:从一个粗糙的实现开始,一步步暴露问题,再逐步引入解耦、幂等、频控、队列等工程思想,最终让它成长为一个可靠的事件驱动通知系统

        希望读完后,你再看“发通知”这类需求时,眼里不再是“几行 if 和 API 调用”,而是一幅可以持续演进的系统蓝图。

一、业务背景:一个朴素的库存提醒需求

1.1 需求描述

在商城后台,商家维护商品库存。当某个商品的库存数量跌到预设的预警阈值以下时,系统需要主动通知商家。

具体场景:

  • 商品:iPhone 15

  • 当前库存:3 件

  • 预警阈值:5 件

  • 触发动作:通过微信、邮件发送“库存不足”提醒

通知内容通常包括商品名称、当前库存、阈值,可能还有补货链接。

1.2 初期实现思路(我曾经的写法)

我当时的直觉是:库存扣减完,顺手判断一下,如果低于阈值,就直接调用通知接口。

流程可以画成一条直线:

text

用户下单 → 扣减库存 → 判断库存 < 阈值 ? → 调用微信API → 发送邮件

对应的 Laravel 代码大致长这样:

php

public function decreaseStock(Product $product, int $quantity)
{
    $product->stock -= $quantity;
    $product->save();

    if ($product->stock < $product->threshold) {
        // 直接发送微信
        WechatService::sendTemplateMessage(
            $product->merchant->wechat_openid,
            "商品 {$product->name} 库存不足,当前仅剩 {$product->stock} 件"
        );
        // 直接发送邮件
        Mail::to($product->merchant->email)->send(
            new LowStockMail($product)
        );
    }
}

上线之后,确实能发通知。但很快,噩梦开始了。

二、直接调用通知服务:埋下的四个深坑

2.1 业务代码与通知实现“焊”在一起

库存模块的 decreaseStock() 里,既要知道微信怎么发(需要 openid、模板 ID),又要知道邮件怎么发(要构造 LowStockMail 对象)。将来产品说“再加个短信通知”或者“接入企业微信机器人”,我就得回来改 decreaseStock()

违反的正是开闭原则(对扩展开放,对修改关闭)。库存模块本不该关心“用什么渠道发通知”,它只需要说一句“库存低了”就够了。

2.2 通知失败会拖垮核心交易

有一次微信接口突然超时(整整 3 秒才返回错误),而 decreaseStock() 是同步调用的,导致用户下单接口响应时间从 200ms 暴涨到 3.2 秒。更可怕的是,因为异常未捕获,整个事务回滚了 —— 库存没扣成,订单却创建了(因为订单在另一个事务里)。最终库存超卖。

通知不该影响核心业务,这是血的教训。

2.3 无法应对复杂的业务规则

几个月后,产品又提了新需求:

  • 同一商家,每天最多通知 10 次(防止骚扰);

  • 同一商品,30 分钟内不重复提醒(避免反复发送);

  • 商家可以在后台“关闭通知”。

如果继续往 decreaseStock() 里塞这些逻辑,代码很快就变成一锅粥。

2.4 扩展性为零

后来公司引入了新的通知渠道(如钉钉、APP 推送),每次都要修改库存模块。通知渠道的变更,竟然要牵动核心业务代码,这显然不合理。

这些问题其实都可以归结为两个字:耦合

三、引入事件驱动:第一次优雅转身

3.1 事件驱动是什么?

事件驱动的核心思想很简单:一个业务动作发生后,只发布一个“事件”,至于这个事件被谁处理、怎么处理,发布者一概不管。

用库存通知为例:

  • 库存扣减完成后,如果低于阈值,就发布一个 LowStockEvent

  • 至于谁负责发微信、发邮件、记日志,那是事件监听器的事。

库存模块从此“两耳不闻窗外事”,只负责自己的核心职责。

3.2 新架构的示意图

text

┌─────────────┐     发布事件      ┌─────────────────┐
│  库存服务   │ ───────────────▶ │  LowStockEvent   │
│ (扣减库存)  │                   └────────┬────────┘
└─────────────┘                            │
                                    ┌───────┴────────┐
                                    │  事件监听器     │
                                    └───────┬────────┘
                                            │
                          ┌─────────────────┼─────────────────┐
                          ▼                 ▼                 ▼
                    ┌──────────┐     ┌──────────┐     ┌──────────┐
                    │微信通知  │     │邮件通知  │     │日志记录  │
                    └──────────┘     └──────────┘     └──────────┘

3.3 Laravel 中的落地代码

定义事件(它只是一个携带数据的 DTO):

php

namespace App\Events;

use App\Models\Product;

class LowStockEvent
{
    public Product $product;

    public function __construct(Product $product)
    {
        $this->product = $product;
    }
}

在库存扣减方法中发布事件

php

public function decreaseStock(Product $product, int $quantity)
{
    $product->stock -= $quantity;
    $product->save();

    if ($product->stock < $product->threshold) {
        event(new LowStockEvent($product));   // 仅发布事件
    }
}

创建监听器(独立于库存服务):

php

namespace App\Listeners;

use App\Events\LowStockEvent;
use App\Services\NotificationService;

class LowStockListener
{
    public function handle(LowStockEvent $event)
    {
        NotificationService::sendLowStockAlert($event->product);
    }
}

然后在 EventServiceProvider 中注册监听关系。从此,库存服务不再知道“通知”二字。将来增加短信、钉钉,只需要添加新的监听器,库存代码纹丝不动。

效果:耦合解除了,但问题也才刚刚开始。

四、通知模块职责划分:别造“万能类”

解耦之后,我把所有通知逻辑塞进了 NotificationService,它既判断重复、又统计次数、还负责选渠道、发消息。两个月后,这个类膨胀到 800 行,改一个渠道可能影响另一个,测试都不敢动。

4.1 错误的设计

text

┌──────────────────────────────────────┐
│        NotificationService           │
│  - 判断库存是否低于阈值(重复业务)   │
│  - 查询今日已发次数                   │
│  - 检查是否重复发送                   │
│  - 选择发送渠道                       │
│  - 调用微信/邮件/短信 API             │
│  - 记录发送日志                       │
└──────────────────────────────────────┘

这个类既管“该不该发”,又管“怎么发”,违反单一职责。

4.2 正确的分层

我把它拆成两层:

业务决策层(监听器 / 服务)
负责判断“是否应该发送”:

  • 库存真的低于阈值了吗?

  • 今天发送次数超限了吗?

  • 距离上次发送是否超过 30 分钟?

  • 商家关闭通知了吗?

通知执行层(渠道服务)
只负责“如何发送”:

  • WechatChannel::send($message)

  • EmailChannel::send($message)

  • SmsChannel::send($message)

这样,决策逻辑与渠道实现彻底分离。以后调整频控策略,只改业务层;更换短信供应商,只改渠道层。

php

// 监听器(业务决策)
class LowStockListener
{
    public function handle(LowStockEvent $event)
    {
        if (! $this->shouldSend($event->product)) {
            return;
        }

        $message = $this->buildMessage($event->product);
        app(NotificationDispatcher::class)->send($message);
    }

    private function shouldSend(Product $product): bool
    {
        // 检查商家是否开启通知、今日次数、重复间隔……
        return true;
    }
}

// 分发器(选择渠道)
class NotificationDispatcher
{
    public function send(Message $message)
    {
        foreach ($this->channels as $channel) {
            $channel->send($message);
        }
    }
}

五、消息幂等:如何避免商家被“轰炸”

5.1 重复通知的源头

商品库存不是一次性从 100 降到 0,而是一点点减少。比如:

  • 10:00 库存从 10 变为 4(低于阈值 5) → 发送一次;

  • 10:05 库存从 4 变为 3(还是低于阈值) → 又发送一次;

  • 10:10 库存从 3 变为 2 → 第三次……

商家在 10 分钟内收到三条几乎一样的“库存不足”提醒,必然投诉。

5.2 幂等方案:通过通知记录去重

引入一张 notification_records 表,记录每一次已发送的通知关键信息:

merchant_id product_id type sent_at
1001 88001 low_stock 2026-08-08 10:00:00

在发送前,查询最近 30 分钟内是否已有相同商品、相同类型的通知记录。若有,则跳过本次发送。

php

private function shouldSend(Product $product): bool
{
    $recent = NotificationRecord::where('merchant_id', $product->merchant_id)
        ->where('product_id', $product->id)
        ->where('type', 'low_stock')
        ->where('sent_at', '>=', now()->subMinutes(30))
        ->exists();

    return ! $recent && /* 其他条件 */;
}

5.3 幂等思想的价值

幂等性(Idempotence)是分布式系统里的核心概念:同一个操作,执行一次和执行多次,结果保持不变。在支付回调、消息队列消费、订单处理中,幂等设计是防重复的关键。在这里我们只不过把思想用到了一个“小通知”上,但它背后的工程价值是相通的。

简单的去重,背后是严谨的幂等哲学。

六、频控设计:别把第三方服务打垮

6.1 为什么要限流?

假设某天由于系统异常,库存服务一次性生成了 10000 个 LowStockEvent,监听器会疯狂调用微信和邮件接口。微信接口限流,直接封禁我们的 IP;邮件服务商也报警了。更严重的是,商家手机被 10000 条微信炸得死机。

6.2 频控应该放在哪?

错误做法:把限流逻辑放到 WechatChannel 里,让渠道服务自己计数。但渠道服务不该理解“今天能发几条”这种业务规则。

正确做法:放在业务决策层(监听器或专门的中介服务),在调用渠道之前进行频控检查。

php

private function shouldSend(Product $product): bool
{
    // 1. 检查重复
    // 2. 检查今日总次数
    $todayCount = NotificationRecord::where('merchant_id', $product->merchant_id)
        ->where('type', 'low_stock')
        ->whereDate('sent_at', today())
        ->count();

    if ($todayCount >= 10) {
        return false;
    }

    return true;
}

频控策略可以根据商户等级动态配置,未来甚至可以做到滑动窗口等更复杂的算法,但无论如何,决策层要清晰。

七、系统进一步演进:消息队列与异步化

当业务量再上一个台阶,事件监听器同步执行可能成为瓶颈。比如一次库存变化触发了 5 个监听器(微信、邮件、日志、数据分析、风控),总执行时间可能超过 1 秒,影响用户体验。

这时候,我们可以将事件发布到消息队列,由独立的消费者异步处理。

7.1 异步架构图

text

┌─────────────┐   发布事件   ┌──────────────┐    投递    ┌─────────────┐
│  库存服务   │────────────▶│ 消息队列     │───────────▶│ 通知消费者  │
└─────────────┘             │ (Redis/Kafka)│            └──────┬──────┘
                            └──────────────┘                   │
                                                       ┌───────┴───────┐
                                                       │ 微信 / 邮件   │
                                                       └───────────────┘

7.2 Laravel 中的队列化

Laravel 的事件系统天然支持队列,只需让监听器实现 ShouldQueue 接口:

php

use Illuminate\Contracts\Queue\ShouldQueue;

class LowStockListener implements ShouldQueue
{
    public function handle(LowStockEvent $event)
    {
        // 该监听器将被推送到队列,异步执行
    }
}

优势:

  1. 异步:库存扣减后立即返回,不等待通知完成,响应更快。

  2. 失败重试:如果微信接口临时不可用,队列可以自动重试(可配置次数和间隔)。

  3. 削峰填谷:大量通知堆积时,消费者以可控速度消费,保护下游依赖。

  4. 可观测性:队列监控可以直观看到积压情况,方便扩容。

当然,异步也带来新挑战(如消息丢失、重复消费、顺序性问题),但这些都有成熟的解决方案,是工程师成长路上的必修课。

八、从一个通知功能,看后端工程能力成长地图

回顾整个过程,我把它整理成一条成长曲线:

阶段 做法 问题 解决方案
阶段1 在业务方法里直接调 API 耦合严重、影响核心业务 事件驱动解耦
阶段2 把所有逻辑塞进一个通知类 职责混乱、难以维护 分层设计(决策层/执行层)
阶段3 重复发送,骚扰商家 体验差、浪费资源 幂等 + 通知记录
阶段4 瞬时流量打垮第三方 系统不可用 频控 + 限流
阶段5 同步执行拖慢响应 性能瓶颈 消息队列异步化

每一个阶段都是对“工程思维”的锤炼。从“能跑就行”到“可靠、可维护、可扩展”,这个转变不是靠背诵设计模式,而是靠一次次踩坑后的反思。

真正优秀的后端工程师,不是能最快实现需求的人,而是能在设计阶段就预见到变化、给系统留出呼吸空间的人。

九、总结:从“堆代码”到“做设计”

写这篇文章时,我一直在想:如果当初接手那个“简单库存提醒”时,我能有现在的认知,会少走多少弯路?

一个看似不起眼的通知功能,串联起了事件驱动、解耦、幂等、频控、异步队列等一整套思想。这些思想并不仅限于“库存通知”,它们是构建任何可靠分布式系统的基石。

下次当你接到一个“发消息”的需求时,不妨问自己几个问题:

  • 如果通知服务挂了,会影响核心业务吗?

  • 重复发消息怎么办?

  • 流量洪峰来了,下游扛得住吗?

  • 今天加短信,明天加钉钉,我的代码需要改多少?

把这些问题的答案写进你的设计文档,而不是堆进代码里。你的系统,会感谢你。

希望这篇文章能给你带来一些启发。如果你也在实践中遇到过类似的困惑,欢迎在评论区交流你的故事。 🚀

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐