Files
saasshop/app/Support/SubscriptionActivationService.php

139 lines
5.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Support;
use App\Models\PlatformOrder;
use App\Models\SiteSubscription;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Str;
/**
* 订阅激活服务(最小闭环)
*
* 目标:当平台订单满足“已支付 + 已生效”时,让站点订阅真正生效或续期。
*
* 约束:
* - 只负责业务层状态/时间的联动,不负责支付回执、异步通知等入口。
* - 支持两种情况:
* 1) 订单已绑定 site_subscription_id => 续期/延长该订阅
* 2) 未绑定 => 创建新订阅并回写订单 site_subscription_id
*/
class SubscriptionActivationService
{
/**
* @throws ModelNotFoundException
*/
public function activateOrder(int $orderId, ?int $adminId = null): SiteSubscription
{
/** @var PlatformOrder $order */
$order = PlatformOrder::query()->findOrFail($orderId);
// 最小校验:必须已支付 + 已生效
if ($order->payment_status !== 'paid' || $order->status !== 'activated') {
throw new \InvalidArgumentException('订单未满足生效条件:需 payment_status=paid 且 status=activated');
}
$now = now();
$months = max(1, (int) $order->period_months) * max(1, (int) ($order->quantity ?? 1));
// 幂等保护:若该订单已同步过订阅,则直接返回对应订阅(避免重复点击导致无限续期)
$activationMeta = (array) data_get($order->meta ?? [], 'subscription_activation', []);
$activatedSubscriptionId = (int) ($activationMeta['subscription_id'] ?? 0);
if ($activatedSubscriptionId > 0) {
$subscription = SiteSubscription::query()->find($activatedSubscriptionId);
if ($subscription) {
return $subscription;
}
}
// 订阅快照:优先使用订单上记录的 plan_name / billing_cycle / plan_snapshot
$snapshot = [
'from_order_id' => $order->id,
'order_no' => $order->order_no,
'order_type' => $order->order_type,
'plan_snapshot' => $order->plan_snapshot,
];
if ($order->site_subscription_id) {
/** @var SiteSubscription $subscription */
$subscription = SiteSubscription::query()->findOrFail($order->site_subscription_id);
// 以 ends_at 为基准续期:
// - 若 ends_at 为空或已过期 => 从 now 起算
// - 若仍有效 => 从 ends_at 起算
$base = $subscription->ends_at && $subscription->ends_at->greaterThan($now)
? $subscription->ends_at->copy()
: $now->copy();
$newEndsAt = $base->copy()->addMonthsNoOverflow($months);
// starts_at若为空则补齐
$startsAt = $subscription->starts_at ?: $now->copy();
$subscription->fill([
'status' => 'activated',
'plan_id' => $order->plan_id,
'plan_name' => $order->plan_name,
'billing_cycle' => $order->billing_cycle,
'period_months' => (int) $order->period_months,
'amount' => $order->paid_amount > 0 ? $order->paid_amount : $order->payable_amount,
'starts_at' => $startsAt,
'ends_at' => $newEndsAt,
'activated_at' => $subscription->activated_at ?: $now,
'snapshot' => array_merge((array) ($subscription->snapshot ?? []), $snapshot),
]);
$subscription->save();
// 写入订单 meta标记该订单已完成订阅同步幂等
$meta = (array) ($order->meta ?? []);
data_set($meta, 'subscription_activation', [
'subscription_id' => $subscription->id,
'synced_at' => $now->toDateTimeString(),
'admin_id' => $adminId,
]);
$order->meta = $meta;
$order->save();
return $subscription;
}
// 创建新订阅
$subscriptionNo = 'SUB' . $now->format('YmdHis') . Str::padLeft((string) random_int(1, 9999), 4, '0');
$subscription = SiteSubscription::query()->create([
'merchant_id' => $order->merchant_id,
'plan_id' => $order->plan_id,
'status' => 'activated',
'source' => 'platform_order',
'subscription_no' => $subscriptionNo,
'plan_name' => $order->plan_name,
'billing_cycle' => $order->billing_cycle,
'period_months' => (int) $order->period_months,
'amount' => $order->paid_amount > 0 ? $order->paid_amount : $order->payable_amount,
'starts_at' => $now,
'ends_at' => $now->copy()->addMonthsNoOverflow($months),
'activated_at' => $now,
'snapshot' => $snapshot,
'meta' => [
'activated_by_admin_id' => $adminId,
],
]);
// 回写订单 + 写入 meta 标记同步完成(幂等)
$order->site_subscription_id = $subscription->id;
$order->activated_at = $order->activated_at ?: $now;
$meta = (array) ($order->meta ?? []);
data_set($meta, 'subscription_activation', [
'subscription_id' => $subscription->id,
'synced_at' => $now->toDateTimeString(),
'admin_id' => $adminId,
]);
$order->meta = $meta;
$order->save();
return $subscription;
}
}