php-montsers/sasscription

一个简单的界面,用于管理基于Laravel的SaaS应用中的订阅和功能使用。

1.0.0 2024-09-24 21:37 UTC

This package is auto-updated.

Last update: 2024-09-24 21:39:43 UTC


README

SAAS Subscription

Sasscription (SAAS Subscription)

一个灵活的Laravel包,用于独立项目和多租户SAAS应用中的无缝订阅管理。它支持计费、订阅计划和项目消耗,提供管理用户订阅的完整解决方案。

要求

  • PHP 8.3+
  • Laravel 11+

安装

您可以通过composer安装此包

composer require php-montsers/sasscription

包迁移会自动加载,但您仍然可以使用此命令发布它们

php artisan vendor:publish --tag="sasscription-migrations"
php artisan migrate

使用

要开始使用,您只需将给定的特质添加到您的User模型(或任何您希望具有订阅的实体)

<?php

namespace App\Models;

use PhpMonsters\Sasscription\Models\Concerns\HasSubscriptions;

class User
{
    use HasSubscriptions;
}

就这样!

设置功能

首先,您必须定义您将提供的功能。在下面的示例中,我们创建了两个功能:一个用于处理每个用户可以使用部署的分钟数以及他们是否可以使用子域。

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use PhpMonsters\Sasscription\Enums\PeriodicityType;
use PhpMonsters\Sasscription\Models\Feature;

class FeatureSeeder extends Seeder
{
    public function run()
    {
        $deployMinutes = Feature::create([
            'consumable'       => true,
            'name'             => 'deploy-minutes',
            'periodicity_type' => PeriodicityType::Day,
            'periodicity'      => 1,
        ]);

        $customDomain = Feature::create([
            'consumable' => false,
            'name'       => 'custom-domain',
        ]);
    }
}

通过说deploy-minutes是一个可消耗的功能,我们是在告诉用户他们可以使用它有限次数(或直到给定的数量)。另一方面,通过传递PeriodicityType::Day和1作为其periodicity_typeperiodicity,我们表示它应该每天更新。因此,用户今天可以使用他们的分钟数,明天再得到它,例如。

需要注意的是,计划和可消耗功能都有其周期性,因此您的用户可以拥有月度计划,同时具有周功能。

我们定义的另一个功能是$customDomain,它是一个不可消耗的功能。不可消耗的功能意味着用户可以执行特定的操作(在这个例子中,使用自定义域)。

后付费功能

您可以设置一个功能,使其可以在其费用之上使用。要这样做,您只需将postpaid属性设置为true

$cpuUsage = Feature::create([
    'consumable' => true,
    'postpaid'   => true,
    'name'       => 'cpu-usage',
]);

这样,用户将能够使用该功能直到期末,即使他没有足够的费用来使用它(您可以稍后向他收费)。

配额功能

例如,当创建文件存储系统时,您将需要在用户上传和删除文件时增加和减少功能消耗。为了轻松实现这一点,您可以使用配额功能。这些功能具有独特的、不可失效的消耗,因此可以反映一个恒定的值(如本例中使用的系统存储)。

class FeatureSeeder extends Seeder
{
    public function run()
    {
        $storage = Feature::create([
            'consumable' => true,
            'quota'      => true,
            'name'       => 'storage',
        ]);
    }
}

...

class PhotoController extends Controller
{
    public function store(Request $request)
    {
        $userFolder = auth()->id() . '-files';

        $request->file->store($userFolder);

        $usedSpace = collect(Storage::allFiles($userFolder))
            ->map(fn (string $subFile) => Storage::size($subFile))
            ->sum();

        auth()->user()->setConsumedQuota('storage', $usedSpace);

        return redirect()->route('files.index');
    }
}

在上面的示例中,我们在seeder中将storage设置为配额功能。然后,在控制器中,我们的代码将上传的文件存储在文件夹中,通过检索其所有子文件来计算这个文件夹的大小,最后将消耗的storage配额设置为目录的总大小。

创建计划

现在您需要定义您应用中可用的订阅计划

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use PhpMonsters\Sasscription\Enums\PeriodicityType;
use PhpMonsters\Sasscription\Models\Plan;

class PlanSeeder extends Seeder
{
    public function run()
    {
        $bronze = Plan::create([
            'name'             => 'bronze',
            'periodicity_type' => PeriodicityType::Month,
            'periodicity'      => 1,
        ]);
        
        $silver = Plan::create([
            'name'             => 'silver',
            'periodicity_type' => PeriodicityType::Month,
            'periodicity'      => 1,
        ]);

        $gold = Plan::create([
            'name'             => 'gold',
            'periodicity_type' => PeriodicityType::Month,
            'periodicity'      => 1,
        ]);
    }
}

这里的一切都很简单,但值得强调的是:通过接收上面的周期性选项,这两个计划被定义为月度。

没有周期性的计划(“免费计划”或“永久计划”)

您可以定义无周期性的计划,这样用户可以永久订阅(或者直到他们取消订阅)。要做到这一点,只需将null值传递给periodicity_typeperiodicity属性即可

$free = Plan::create([
    'name'             => 'free',
    'periodicity_type' => null,
    'periodicity'      => null,
]);

宽限期

您可以为每个计划定义一定数量的宽限期,这样您的用户在计划到期后不会立即失去对功能的访问

$gold = Plan::create([
    'name'             => 'gold',
    'periodicity_type' => PeriodicityType::Month,
    'periodicity'      => 1,
    'grace_days'       => 7,
]);

根据上述配置,"黄金"计划的订阅者在计划到期和他们的访问被暂停之间将有七天时间。

将计划与功能关联

由于每个功能可以属于多个计划(并且它们可以有多个功能),您必须将它们关联起来

use PhpMonsters\Sasscription\Models\Feature;

// ...

$deployMinutes = Feature::whereName('deploy-minutes')->first();
$subdomains    = Feature::whereName('subdomains')->first();

$silver->features()->attach($deployMinutes, ['charges' => 15]);

$gold->features()->attach($deployMinutes, ['charges' => 25]);
$gold->features()->attach($subdomains);

在将可消耗功能与计划关联时,必须为charges传递一个值。

在上述示例中,我们为银用户提供15分钟的部署时间,为黄金用户提供25分钟。我们还允许黄金用户使用子域名。

订阅

现在您已经有一组具有自己功能的计划,是时候将用户订阅到它们了。注册订阅相当简单

<?php

namespace App\Listeners;

use App\Events\PaymentApproved;

class SubscribeUser
{
    public function handle(PaymentApproved $event)
    {
        $subscriber = $event->user;
        $plan       = $event->plan;

        $subscriber->subscribeTo($plan);
    }
}

在上面的示例中,我们模拟了一个在用户支付被批准时为其用户订阅的应用程序。很容易看出,subscribeTo方法只需要一个参数:用户订阅的计划。您可以传递其他选项来处理下面将要介绍的特殊情况。

默认情况下,subscribeTo方法会根据计划周期性计算到期日期,所以您不必担心这一点。

定义到期日和开始日期

您可以通过将$expiration参数传递给方法调用来覆盖订阅到期。下面,我们将特定用户的订阅设置为只在下一年到期。

$subscriber->subscribeTo($plan, expiration: today()->addYear());

您还可以定义订阅何时实际开始(默认行为是立即开始)

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StudentStoreFormRequest;
use App\Models\Course;
use App\Models\User;
use PhpMonsters\Sasscription\Models\Plan;

class StudentController extends Controller
{
    public function store(StudentStoreFormRequest $request, Course $course)
    {
        $student = User::make($request->validated());
        $student->course()->associate($course);
        $student->save();

        $plan = Plan::find($request->input('plan_id'));
        $student->subscribeTo($plan, startDate: $course->starts_at);

        return redirect()->route('admin.students.index');
    }
}

在上面的示例中,我们模拟了一个面向学校的应用程序。它必须在学生注册时为其订阅,但也必须确保其订阅只有在课程开始时才生效。

切换计划

用户经常改变主意,您必须处理这种情况。如果您需要更改用户的当前计划,只需调用switchTo方法即可

$student->switchTo($newPlan);

如果您不传递任何参数,该方法将取消当前订阅并立即启动一个新订阅。

此调用将触发SubscriptionStarted(Subscription $subscription)事件。

获取当前余额

如果您需要用户的剩余费用,只需调用balance方法。想象一下这样一个场景:一个学生有一个名为notes-download的可消耗功能。要获取剩余下载限制

$student->balance('notes-download');

这只是一个添加到丰富开发者体验的getRemainingCharges的别名。

安排切换

如果您想保持用户在当前计划下直到其到期,请将$immediately参数传递为false

$primeMonthly = Plan::whereName('prime-monthly')->first();
$user->subscribeTo($primeMonthly);

...

$primeYearly = Plan::whereName('prime-yearly')->first();
$user->switchTo($primeYearly, immediately: false);

在上面的示例中,用户将保持其月度订阅直到到期,然后开始使用年计划。这非常有用,因为您不需要处理部分退款,您只能在当前付费计划到期时向用户收费。

在内部,此调用将创建一个具有与当前到期日相同的开始日期的订阅,因此它不会影响您的应用程序,直到那时。

此调用将触发SubscriptionScheduled(Subscription $subscription)事件。

续订

要续订订阅,只需调用renew()方法

$subscriber->subscription->renew();

此方法将触发SubscriptionRenewed(Subscription $subscription)事件。

它将根据当前日期计算一个新的到期日期。

过期订阅

要检索过期订阅,您可以使用lastSubscription方法

$subscriber->lastSubscription();

此方法将返回用户的最后一个订阅,无论其状态如何,因此您可以,例如,获取一个过期订阅来续订它。

$subscriber->lastSubscription()->renew();

取消

在取消订阅时需要注意一点:它不会立即撤销访问权限。为了避免您需要处理任何类型的退款,我们保持订阅状态并仅将其标记为已取消,这样您就只需在将来不再续订即可。如果您需要立即抑制订阅,请查看suppress()方法。

要取消订阅,请使用cancel()方法

$subscriber->subscription->cancel();

此方法将通过将列canceled_at填充为当前时间戳来标记订阅为已取消。

此方法将触发SubscriptionCanceled(Subscription $subscription)事件。

抑制

要抑制订阅(并立即撤销它),请使用suppress()方法

$subscriber->subscription->suppress();

此方法将通过将列suppressed_at填充为当前时间戳来标记订阅为已抑制。

此方法将触发SubscriptionSuppressed(Subscription $subscription)事件。

开始

要开始订阅,请使用start()方法

$subscriber->subscription->start(); // To start it immediately
$subscriber->subscription->start($startDate); // To determine when to start

当没有传递参数时,此方法将触发SubscriptionStarted(Subscription $subscription)事件,并且当提供的开始日期是未来时,也将触发SubscriptionStarted(Subscription $subscription)事件。

此方法将通过填充列started_at来标记订阅为已开始(或计划开始)。

功能消费

要注册特定功能的消费,只需调用consume方法并传递功能名称和消费量(对于不可消费的功能,您无需提供)

$subscriber->consume('deploy-minutes', 4.5);

此方法将检查功能是否可用,并在它们不可用的情况下抛出异常:OutOfBoundsException如果功能对计划不可用,以及OverflowException如果它可用,但费用不足以覆盖消费。

此调用将触发FeatureConsumed($subscriber, Feature $feature, FeatureConsumption $featureConsumption)事件。

检查可用性

要检查功能是否可供消费,您可以使用以下方法之一

$subscriber->canConsume('deploy-minutes', 10);

检查用户是否可以消费一定量的特定功能(它检查用户是否有权访问该功能以及他是否有足够的剩余费用)

$subscriber->cantConsume('deploy-minutes', 10);

它在内部调用canConsume()方法并反转返回值。

$subscriber->hasFeature('deploy-minutes');

简单地检查用户是否有权访问特定功能(不查找其费用)

$subscriber->missingFeature('deploy-minutes');

类似于cantConsume,它返回hasFeature的反值。

功能券

券是允许您的订阅者获取功能费用的简单方法。当用户收到券时,他可以像在正常订阅中那样消费其费用。券可以用于扩展基于订阅的常规系统(例如,您可以出售更多特定功能的费用)或者甚至构建一个完全预付费的服务,您的用户只需为他们想要使用的付费。

启用券

为了使用此功能,您必须在配置文件中启用券。首先,发布包配置

php artisan vendor:publish --tag="sasscription-config"

最后,打开subscription.php文件并将feature_tickets标志设置为true。就这样,您现在可以使用券了!

创建券

要创建券,您可以使用giveTicketFor方法。此方法期望功能名称、过期时间和可选的收费数量(对于创建不可消费功能的券时可以忽略)

$subscriber->giveTicketFor('deploy-minutes', today()->addMonth(), 10);

此方法将触发FeatureTicketCreated($subscriber, Feature $feature, FeatureTicket $featureTicket)事件。

在上面的示例中,用户将获得额外的十分钟来执行部署,直到下个月。

不可消费功能

您可以创建不可消费功能的券,这样您的订阅者就可以在一定时间内访问它们。

class UserFeatureTrialController extends Controller
{
    public function store(FeatureTrialRequest $request, User $user)
    {
        $featureName = $request->input('feature_name');
        $expiration = today()->addDays($request->input('trial_days'));
        $user->giveTicketFor($featureName, $expiration);

        return redirect()->route('admin.users.show', $user);
    }
}

不可过期票据

您可以创建永不过期的票据,这样您的订阅者将永远有权访问它们。

$subscriber->giveTicketFor('deploy-minutes', null, 10);

当您的用户取消订阅时,别忘了删除这些票据。否则,他们将能够无限期地使用这些费用。

测试

composer test

更新日志

有关最近更改的更多信息,请参阅更新日志

贡献

有关详细信息,请参阅贡献指南

安全漏洞

有关如何报告安全漏洞,请参阅我们的安全策略

许可

MIT许可(MIT)。有关更多信息,请参阅许可文件