mirror of
https://github.com/cedar2025/Xboard.git
synced 2025-01-23 02:58:14 -05:00
194 lines
5.5 KiB
PHP
194 lines
5.5 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Plan;
|
||
use App\Models\User;
|
||
use App\Exceptions\ApiException;
|
||
use Illuminate\Database\Eloquent\Collection;
|
||
|
||
class PlanService
|
||
{
|
||
public Plan $plan;
|
||
|
||
public function __construct(Plan $plan)
|
||
{
|
||
$this->plan = $plan;
|
||
}
|
||
|
||
/**
|
||
* 获取所有可销售的订阅计划列表
|
||
* 条件:show 和 sell 为 true,且容量充足
|
||
*
|
||
* @return Collection
|
||
*/
|
||
public function getAvailablePlans(): Collection
|
||
{
|
||
return Plan::where('show', true)
|
||
->where('sell', true)
|
||
->orderBy('sort')
|
||
->get()
|
||
->filter(function ($plan) {
|
||
return $this->hasCapacity($plan);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取指定订阅计划的可用状态
|
||
* 条件:renew 和 sell 为 true
|
||
*
|
||
* @param int $planId
|
||
* @return Plan|null
|
||
*/
|
||
public function getAvailablePlan(int $planId): ?Plan
|
||
{
|
||
return Plan::where('id', $planId)
|
||
->where('sell', true)
|
||
->where('renew', true)
|
||
->first();
|
||
}
|
||
|
||
/**
|
||
* 检查指定计划是否可用于指定用户
|
||
*
|
||
* @param Plan $plan
|
||
* @param User $user
|
||
* @return bool
|
||
*/
|
||
public function isPlanAvailableForUser(Plan $plan, User $user): bool
|
||
{
|
||
// 如果是续费
|
||
if ($user->plan_id === $plan->id) {
|
||
return $plan->renew;
|
||
}
|
||
|
||
// 如果是新购
|
||
return $plan->show && $plan->sell && $this->hasCapacity($plan);
|
||
}
|
||
|
||
public function validatePurchase(User $user, string $period): void
|
||
{
|
||
if (!$this->plan) {
|
||
throw new ApiException(__('Subscription plan does not exist'));
|
||
}
|
||
|
||
// 转换周期格式为新版格式
|
||
$periodKey = self::getPeriodKey($period);
|
||
|
||
if ($periodKey === Plan::PERIOD_RESET_TRAFFIC) {
|
||
$this->validateResetTrafficPurchase($user);
|
||
return;
|
||
}
|
||
|
||
// 检查价格时使用新版格式
|
||
if (!isset($this->plan->prices[$periodKey])) {
|
||
throw new ApiException(__('This payment period cannot be purchased, please choose another period'));
|
||
}
|
||
|
||
if ($user->plan_id !== $this->plan->id && !$this->hasCapacity($this->plan)) {
|
||
throw new ApiException(__('Current product is sold out'));
|
||
}
|
||
|
||
$this->validatePlanAvailability($user);
|
||
}
|
||
|
||
/**
|
||
* 智能转换周期格式为新版格式
|
||
* 如果是新版格式直接返回,如果是旧版格式则转换为新版格式
|
||
*
|
||
* @param string $period
|
||
* @return string
|
||
*/
|
||
public static function getPeriodKey(string $period): string
|
||
{
|
||
// 如果是新版格式直接返回
|
||
if (in_array($period, self::getNewPeriods())) {
|
||
return $period;
|
||
}
|
||
|
||
// 如果是旧版格式则转换为新版格式
|
||
return Plan::LEGACY_PERIOD_MAPPING[$period] ?? $period;
|
||
}
|
||
/**
|
||
* 只能转换周期格式为旧版本
|
||
*/
|
||
public static function convertToLegacyPeriod(string $period): string
|
||
{
|
||
return Plan::LEGACY_PERIOD_MAPPING[$period] ?? $period;
|
||
}
|
||
|
||
/**
|
||
* 获取所有支持的新版周期格式
|
||
*
|
||
* @return array
|
||
*/
|
||
public static function getNewPeriods(): array
|
||
{
|
||
return array_values(Plan::LEGACY_PERIOD_MAPPING);
|
||
}
|
||
|
||
/**
|
||
* 获取旧版周期格式
|
||
*
|
||
* @param string $period
|
||
* @return string
|
||
*/
|
||
public static function getLegacyPeriod(string $period): string
|
||
{
|
||
$flipped = array_flip(Plan::LEGACY_PERIOD_MAPPING);
|
||
return $flipped[$period] ?? $period;
|
||
}
|
||
|
||
protected function validateResetTrafficPurchase(User $user): void
|
||
{
|
||
if (!app(UserService::class)->isAvailable($user) || $this->plan->id !== $user->plan_id) {
|
||
throw new ApiException(__('Subscription has expired or no active subscription, unable to purchase Data Reset Package'));
|
||
}
|
||
}
|
||
|
||
protected function validatePlanAvailability(User $user): void
|
||
{
|
||
if ((!$this->plan->show && !$this->plan->renew) || (!$this->plan->show && $user->plan_id !== $this->plan->id)) {
|
||
throw new ApiException(__('This subscription has been sold out, please choose another subscription'));
|
||
}
|
||
|
||
if (!$this->plan->renew && $user->plan_id == $this->plan->id) {
|
||
throw new ApiException(__('This subscription cannot be renewed, please change to another subscription'));
|
||
}
|
||
|
||
if (!$this->plan->show && $this->plan->renew && !app(UserService::class)->isAvailable($user)) {
|
||
throw new ApiException(__('This subscription has expired, please change to another subscription'));
|
||
}
|
||
}
|
||
|
||
public function hasCapacity(Plan $plan): bool
|
||
{
|
||
if ($plan->capacity_limit === null) {
|
||
return true;
|
||
}
|
||
|
||
$activeUserCount = User::where('plan_id', $plan->id)
|
||
->where(function ($query) {
|
||
$query->where('expired_at', '>=', time())
|
||
->orWhereNull('expired_at');
|
||
})
|
||
->count();
|
||
|
||
return ($plan->capacity_limit - $activeUserCount) > 0;
|
||
}
|
||
|
||
public function getAvailablePeriods(Plan $plan): array
|
||
{
|
||
return array_filter(
|
||
$plan->getActivePeriods(),
|
||
fn($period) => isset($plan->prices[$period]) && $plan->prices[$period] > 0
|
||
);
|
||
}
|
||
|
||
public function canResetTraffic(Plan $plan): bool
|
||
{
|
||
return $plan->reset_traffic_method !== Plan::RESET_TRAFFIC_NEVER
|
||
&& $plan->getResetTrafficPrice() > 0;
|
||
}
|
||
}
|