chore: init saasshop repo + sql migrations runner + gitee go

This commit is contained in:
萝卜
2026-03-10 11:31:02 +00:00
commit 50f15cdea8
210 changed files with 29534 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
<?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;
}
}