让分类、会员和示例工具共用同一账号,本地即可跑通登录与授权。 Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
4.0 KiB
PHP
130 lines
4.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Order;
|
|
use App\Models\Plan;
|
|
use App\Models\Subscription;
|
|
use App\Models\User;
|
|
use App\Support\Settings;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
class MembershipService
|
|
{
|
|
public function snapshot(User $user, ?string $toolSlug = null): array
|
|
{
|
|
$user->loadMissing('subscription.plan.entitlements');
|
|
$subscription = $user->subscription;
|
|
$status = $this->effectiveStatus($subscription);
|
|
$plan = in_array($status, ['active', 'grace'], true)
|
|
? $subscription?->plan
|
|
: Plan::query()->where('code', 'free')->with('entitlements')->first();
|
|
|
|
$entitlements = [];
|
|
foreach ($plan?->entitlements ?? [] as $item) {
|
|
$entitlements[$item->entitlement_key] = $this->castValue($item->entitlement_value);
|
|
}
|
|
if ($toolSlug) {
|
|
$entitlements = array_filter(
|
|
$entitlements,
|
|
fn ($value, $key) => str_contains($key, ':'.$toolSlug.':')
|
|
|| (! str_starts_with($key, 'tool:') && ! str_starts_with($key, 'quota:')),
|
|
ARRAY_FILTER_USE_BOTH
|
|
);
|
|
}
|
|
|
|
return [
|
|
'plan_code' => $plan->code ?? 'free',
|
|
'plan_name' => $plan->name ?? '免费',
|
|
'status' => $status,
|
|
'starts_at' => $subscription?->starts_at?->toIso8601String(),
|
|
'ends_at' => $subscription?->ends_at?->toIso8601String(),
|
|
'entitlement_version' => $user->entitlement_version,
|
|
'entitlements' => $entitlements,
|
|
];
|
|
}
|
|
|
|
public function effectiveStatus(?Subscription $subscription): string
|
|
{
|
|
if (! $subscription || $subscription->status === 'stopped') {
|
|
return $subscription?->status === 'stopped' ? 'stopped' : 'none';
|
|
}
|
|
if (now()->lt($subscription->ends_at)) {
|
|
return 'active';
|
|
}
|
|
$graceDays = (int) Settings::get('grace_days', '3');
|
|
if (now()->lt($subscription->ends_at->copy()->addDays($graceDays))) {
|
|
return 'grace';
|
|
}
|
|
|
|
return 'expired';
|
|
}
|
|
|
|
public function grant(User $user, Plan $plan, Carbon $endsAt): Subscription
|
|
{
|
|
$subscription = Subscription::query()->updateOrCreate(
|
|
['user_id' => $user->id],
|
|
[
|
|
'plan_id' => $plan->id,
|
|
'status' => 'active',
|
|
'starts_at' => now(),
|
|
'ends_at' => $endsAt,
|
|
]
|
|
);
|
|
$user->increment('entitlement_version');
|
|
$user->refresh();
|
|
|
|
return $subscription;
|
|
}
|
|
|
|
public function extendWithOrder(User $user, Plan $plan): Subscription
|
|
{
|
|
$current = $user->subscription;
|
|
$base = ($current && $current->ends_at && $current->ends_at->isFuture() && $current->status !== 'stopped')
|
|
? $current->ends_at->copy()
|
|
: now();
|
|
$ends = match ($plan->period) {
|
|
'year' => $base->addYear(),
|
|
'lifetime' => $base->addYears(100),
|
|
default => $base->addMonth(),
|
|
};
|
|
|
|
return $this->grant($user, $plan, $ends);
|
|
}
|
|
|
|
public function stop(User $user): void
|
|
{
|
|
$user->subscription?->update(['status' => 'stopped']);
|
|
$user->increment('entitlement_version');
|
|
}
|
|
|
|
public function confirmOrder(Order $order): void
|
|
{
|
|
if ($order->status !== 'pending') {
|
|
abort(422, '只有待支付订单可以确认收款');
|
|
}
|
|
$order->update(['status' => 'paid', 'paid_at' => now()]);
|
|
if ($order->type === 'subscription') {
|
|
$item = $order->items()->with('plan')->first();
|
|
if ($item?->plan) {
|
|
$this->extendWithOrder($order->user, $item->plan);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function castValue(string $value): bool|int|string
|
|
{
|
|
if ($value === 'true') {
|
|
return true;
|
|
}
|
|
if ($value === 'false') {
|
|
return false;
|
|
}
|
|
if (preg_match('/^-?\d+$/', $value)) {
|
|
return (int) $value;
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
}
|