You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何合理规划Laravel项目结构实现订阅提醒与自动续费功能

Laravel订阅业务模块代码结构方案

首先纠正一个普遍的认知偏差:"不要把业务逻辑写在Model里"指的是不要把跨依赖、有外部副作用的流程逻辑塞到模型中,和模型自身状态强相关的内聚判断逻辑,本身就应该归属于模型。

1. 状态判断逻辑直接内聚到Subscription模型

你目前放在SubscriptionHelper里的isExpiring()方法,本质是对单个订阅实例自身状态的判断,完全不需要单独抽静态工具类,直接定义在Subscription模型中即可,调用时直接使用$subscription->isExpiring(),可读性、可维护性远高于静态传参调用。
优化后的状态判断代码直接写在Subscription模型内:

public function isExpiring(): bool
{
    // 提前过滤已取消、已过期的无效订阅,避免无效提醒
    if ($this->isCancelled() || $this->isExpired()) {
        return false;
    }
    return $this->getEndDate()->floatDiffInMonths(today()) <= 2;
}

// 其他高频复用的订阅状态判断统一放在模型中
public function isExpired(): bool
{
    return $this->getEndDate()->isPast();
}

public function isAutoRenewEnabled(): bool
{
    return !$this->cancelled_at && $this->auto_renew;
}

// 对应批量查询的场景,可以配套写查询作用域,避免重复写筛选条件
public function scopeExpiringWithin($query, int $months = 2)
{
    return $query->where('end_date', '<=', now()->addMonths($months))
        ->where('end_date', '>', now());
}

把模型当成纯数据结构、所有逻辑都抽到Helper/Service的写法是反模式,单实例的状态判断是模型最核心的职责之一,内聚在模型内部才符合面向对象的封装原则。

2. 流程类逻辑保留在SubscriptionManager服务类

你已经创建的SubscriptionManager定位完全正确,notifyExpiring($subscription)、renewExpired($subscription)这类需要依赖外部服务、包含多步流程、有副作用的逻辑,就应该放在服务类中:

  • 发送到期提醒需要依赖邮件/短信通道、队列服务
  • 自动续费需要依赖支付网关、订单生成、数据库事务、幂等校验
    这类逻辑如果塞到模型里,会导致模型和外部服务强耦合,难以测试和维护。
    服务类中只需要直接调用模型自带的状态判断方法即可,不需要依赖任何静态Helper:
// 每日批量处理到期提醒
public function processExpiringNotifications()
{
    Subscription::query()
        ->active()
        ->expiringWithin(2)
        ->where('expiry_notified_at', null)
        ->chunkById(100, function ($subscriptions) {
            foreach ($subscriptions as $subscription) {
                if ($subscription->isExpiring()) {
                    $this->notifyExpiring($subscription);
                }
            }
        });
}

// 每日批量处理自动续费
public function processAutoRenewals()
{
    Subscription::query()
        ->active()
        ->where('end_date', '<=', now())
        ->where('renewed_at', null)
        ->chunkById(100, function ($subscriptions) {
            foreach ($subscriptions as $subscription) {
                if ($subscription->isAutoRenewEnabled()) {
                    $this->renewExpired($subscription);
                }
            }
        });
}

3. 任务调度配置

两个核心能力不需要额外做入口,直接在Laravel的app/Console/Kernel.php中注册定时任务即可:

  • 到期提醒任务:每日执行一次processExpiringNotifications(),注意给已发送提醒的订阅打上expiry_notified_at标记,避免重复发送
  • 自动续费任务:每日执行一次processAutoRenewals(),注意加事务和幂等锁,避免重复扣款

4. 现有静态Helper类处理建议

直接废弃即可。静态工具类是过程式编程的残留,针对模型状态判断的场景没有任何存在价值:静态方法难以做Mock测试、调用时需要手动传参、逻辑散落在全局文件中难以维护,远不如内聚在模型中直观。

内容的提问来源于stack exchange,提问作者aluxf

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 15:36:16