144 lines
5.8 KiB
PHP
144 lines
5.8 KiB
PHP
<?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);
|
||
|
||
// 治理安全阀:订单绑定的订阅必须属于同一站点(merchant),否则拒绝同步,避免误续费/串单。
|
||
if ((int) $subscription->merchant_id !== (int) $order->merchant_id) {
|
||
throw new \InvalidArgumentException('订阅与订单站点不一致:请核对订阅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;
|
||
}
|
||
}
|