vip会员支持补差升级
This commit is contained in:
+37
-1
@@ -73,11 +73,47 @@ class VipLevel extends Model
|
||||
'exp_multiplier' => 'float',
|
||||
'jjb_multiplier' => 'float',
|
||||
'sort_order' => 'integer',
|
||||
'price' => 'integer',
|
||||
'price' => 'float',
|
||||
'duration_days' => 'integer',
|
||||
'allow_custom_messages' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* 判断当前等级是否高于指定等级。
|
||||
* 依靠 sort_order 判断。
|
||||
*/
|
||||
public function isHigherThan(self|int|null $other): bool
|
||||
{
|
||||
if ($other === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$otherOrder = ($other instanceof self)
|
||||
? $other->sort_order
|
||||
: self::where('id', $other)->value('sort_order') ?? 0;
|
||||
|
||||
return $this->sort_order > $otherOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算相对于另一个等级的差价。
|
||||
* 如果当前等级价格更低,则返回 0。
|
||||
*/
|
||||
public function getUpgradePrice(self|int|null $other): float
|
||||
{
|
||||
if ($other === null) {
|
||||
return (float) $this->price;
|
||||
}
|
||||
|
||||
$otherPrice = ($other instanceof self)
|
||||
? (float) $other->price
|
||||
: (float) (self::where('id', $other)->value('price') ?? 0);
|
||||
|
||||
$diff = (float) $this->price - $otherPrice;
|
||||
|
||||
return max(0.0, $diff);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联:该等级下的所有用户
|
||||
*/
|
||||
|
||||
@@ -35,8 +35,26 @@ class VipPaymentService
|
||||
*/
|
||||
public function createLocalOrder(User $user, VipLevel $vipLevel): VipPaymentOrder
|
||||
{
|
||||
if ((float) $vipLevel->price <= 0) {
|
||||
throw new RuntimeException('当前 VIP 等级未设置在线支付价格,暂不支持直接购买。');
|
||||
$currentVip = $user->isVip() ? $user->vipLevel : null;
|
||||
$isUpgrade = $currentVip && $vipLevel->isHigherThan($currentVip);
|
||||
|
||||
// 如果已经是该等级或更高级别,且不是永久会员续费(逻辑上续费应该用原价,但此处 user 需求是升级补差价)
|
||||
// 这里我们主要处理补差价升级逻辑。
|
||||
$price = $isUpgrade
|
||||
? $vipLevel->getUpgradePrice($currentVip)
|
||||
: (float) $vipLevel->price;
|
||||
|
||||
if ($price < 0.01) {
|
||||
// 如果差价极小或为 0(例如同级或降级),抛出异常或根据业务逻辑处理
|
||||
if ($isUpgrade) {
|
||||
throw new RuntimeException('当前等级差价不足 0.01 元,无法发起升级。');
|
||||
}
|
||||
if ($user->vip_level_id === $vipLevel->id) {
|
||||
// 续费逻辑保持原价
|
||||
$price = (float) $vipLevel->price;
|
||||
} else {
|
||||
throw new RuntimeException('不支持降级购买会员。');
|
||||
}
|
||||
}
|
||||
|
||||
return VipPaymentOrder::create([
|
||||
@@ -45,13 +63,15 @@ class VipPaymentService
|
||||
'user_id' => $user->id,
|
||||
'vip_level_id' => $vipLevel->id,
|
||||
'status' => 'created',
|
||||
'amount' => $vipLevel->price,
|
||||
'subject' => '购买 VIP 会员 - '.$vipLevel->name,
|
||||
'amount' => $price,
|
||||
'subject' => ($isUpgrade ? '【升级】' : '购买') . ' VIP 会员 - ' . $vipLevel->name,
|
||||
'provider' => 'alipay',
|
||||
'vip_name' => $vipLevel->name,
|
||||
'vip_duration_days' => (int) $vipLevel->duration_days,
|
||||
'meta' => [
|
||||
'username' => $user->username,
|
||||
'is_upgrade' => $isUpgrade,
|
||||
'old_vip_level_id' => $currentVip?->id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -188,7 +208,11 @@ class VipPaymentService
|
||||
if (! $lockedOrder->isVipOpened()) {
|
||||
// 只在首次成功支付时开通会员,防止重复回调导致会员时长重复叠加。
|
||||
$user = User::query()->lockForUpdate()->findOrFail($lockedOrder->user_id);
|
||||
$this->vipService->grantVip($user, $lockedOrder->vip_level_id, (int) $lockedOrder->vip_duration_days);
|
||||
|
||||
// 从 meta 中提取是否是升级
|
||||
$isUpgrade = (bool) ($lockedOrder->meta['is_upgrade'] ?? false);
|
||||
|
||||
$this->vipService->grantVip($user, $lockedOrder->vip_level_id, (int) $lockedOrder->vip_duration_days, $isUpgrade);
|
||||
|
||||
$lockedOrder->update([
|
||||
'opened_vip_at' => now(),
|
||||
|
||||
@@ -46,17 +46,24 @@ class VipService
|
||||
* @param User $user 目标用户
|
||||
* @param int $vipLevelId VIP 等级 ID
|
||||
* @param int $days 天数(0=永久)
|
||||
* @param bool $isUpgrade 是否为补差价升级
|
||||
*/
|
||||
public function grantVip(User $user, int $vipLevelId, int $days = 30): void
|
||||
public function grantVip(User $user, int $vipLevelId, int $days = 30, bool $isUpgrade = false): void
|
||||
{
|
||||
$oldVipId = $user->vip_level_id;
|
||||
$user->vip_level_id = $vipLevelId;
|
||||
|
||||
if ($days > 0) {
|
||||
// 如果用户已有未过期的会员,在现有到期时间上延长
|
||||
$baseTime = ($user->hy_time && $user->hy_time->isFuture())
|
||||
? $user->hy_time
|
||||
: now();
|
||||
$user->hy_time = $baseTime->addDays($days);
|
||||
if ($isUpgrade && $oldVipId && $user->hy_time && $user->hy_time->isFuture()) {
|
||||
// 如果是升级,到期日期保持不变,除非新等级是永久(days=0)
|
||||
// 此时只需更新等级 ID,无需修改 hy_time。
|
||||
} else {
|
||||
// 如果是新购或续费,在现有到期时间上延长
|
||||
$baseTime = ($user->hy_time && $user->hy_time->isFuture())
|
||||
? $user->hy_time
|
||||
: now();
|
||||
$user->hy_time = $baseTime->addDays($days);
|
||||
}
|
||||
} else {
|
||||
// 永久会员
|
||||
$user->hy_time = null;
|
||||
|
||||
@@ -87,53 +87,94 @@
|
||||
@foreach ($vipLevels as $vip)
|
||||
@php
|
||||
$isCurrentVipLevel = $user->isVip() && (int) $user->vip_level_id === (int) $vip->id;
|
||||
$currentVip = $user->isVip() ? $user->vipLevel : null;
|
||||
|
||||
// 逻辑:
|
||||
// 1. 如果用户不是 VIP,显示「购买」原价。
|
||||
// 2. 如果用户已经是该等级,显示「续费」原价。
|
||||
// 3. 如果用户是更高级别,显示「无法降级」。
|
||||
// 4. 如果用户是更低级别,显示「升级」差价。
|
||||
|
||||
$isHigher = $currentVip ? $vip->isHigherThan($currentVip) : false;
|
||||
$isLower = $currentVip && (int) $user->vip_level_id !== (int) $vip->id && ! $isHigher;
|
||||
$canUpgrade = $currentVip && $isHigher;
|
||||
$upgradePrice = $canUpgrade ? $vip->getUpgradePrice($currentVip) : (float) $vip->price;
|
||||
|
||||
$btnText = '立即购买';
|
||||
$btnColor = 'bg-gray-900 hover:bg-black text-white';
|
||||
$showUpgradeInfo = false;
|
||||
$isDisabled = false;
|
||||
|
||||
if ($isCurrentVipLevel) {
|
||||
$btnText = '续费会员';
|
||||
$btnColor = 'bg-amber-500 hover:bg-amber-600 text-white';
|
||||
} elseif ($canUpgrade) {
|
||||
$btnText = '补差价升级';
|
||||
$btnColor = 'bg-indigo-600 hover:bg-indigo-700 text-white';
|
||||
$showUpgradeInfo = true;
|
||||
} elseif ($isLower) {
|
||||
$btnText = '无法降级';
|
||||
$btnColor = 'bg-gray-200 text-gray-400 cursor-not-allowed';
|
||||
$isDisabled = true;
|
||||
}
|
||||
@endphp
|
||||
<div class="rounded-3xl border {{ $isCurrentVipLevel ? 'border-amber-300 bg-amber-50/60' : 'border-gray-100 bg-white' }} shadow-sm p-5 flex flex-col">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 rounded-2xl bg-white border border-gray-100 flex items-center justify-center text-2xl shadow-sm">{{ $vip->icon }}</div>
|
||||
<div class="w-12 h-12 rounded-2xl flex items-center justify-center text-2xl" style="background: {{ $vip->color }}20;">
|
||||
{{ $vip->icon }}
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-lg font-extrabold" style="color: {{ $vip->color ?: '#111827' }}">{{ $vip->name }}</h4>
|
||||
<p class="text-xs text-gray-500">当前已有 {{ $vip->users_count }} 位用户使用</p>
|
||||
<h4 class="font-bold text-gray-900">{{ $vip->name }}</h4>
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
<span class="text-xs font-medium text-gray-500">
|
||||
@if ($vip->duration_days > 0)
|
||||
有效期 {{ $vip->duration_days }} 天
|
||||
@else
|
||||
永久有效
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if ($isCurrentVipLevel)
|
||||
<span class="inline-flex px-2.5 py-1 rounded-full bg-amber-500 text-white text-xs font-bold">当前等级</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-5 grid grid-cols-2 gap-3 text-sm">
|
||||
<div class="rounded-2xl bg-indigo-50 border border-indigo-100 p-3">
|
||||
<div class="text-xs text-indigo-500 font-bold">经验倍率</div>
|
||||
<div class="mt-1 text-xl font-extrabold text-indigo-900">×{{ $vip->exp_multiplier }}</div>
|
||||
</div>
|
||||
<div class="rounded-2xl bg-amber-50 border border-amber-100 p-3">
|
||||
<div class="text-xs text-amber-500 font-bold">金币倍率</div>
|
||||
<div class="mt-1 text-xl font-extrabold text-amber-900">×{{ $vip->jjb_multiplier }}</div>
|
||||
<div class="text-right">
|
||||
<div class="text-2xl font-black text-gray-900">
|
||||
<span class="text-sm font-medium">¥</span>{{ number_format($upgradePrice, 2) }}
|
||||
</div>
|
||||
@if ($showUpgradeInfo)
|
||||
<div class="text-[10px] text-indigo-600 font-bold mt-0.5">已省 ¥{{ number_format((float) ($vip->price - $upgradePrice), 2) }}</div>
|
||||
@elseif (! $isCurrentVipLevel && ! $isDisabled)
|
||||
<div class="text-[10px] text-gray-400 font-medium mt-0.5">原价 ¥{{ number_format((float) $vip->price, 2) }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-2 text-sm text-gray-600 flex-1">
|
||||
<div class="flex items-center justify-between"><span>有效时长</span><span class="font-bold text-gray-900">{{ $vip->duration_days > 0 ? $vip->duration_days . ' 天' : '永久' }}</span></div>
|
||||
<div class="flex items-center justify-between"><span>支付金额</span><span class="font-bold text-rose-600">{{ $vip->price > 0 ? '¥' . $vip->price : '联系管理员' }}</span></div>
|
||||
<div class="mt-5 space-y-3 flex-1">
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600">
|
||||
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"></path></svg>
|
||||
<span>经验获取 <b>{{ $vip->exp_multiplier }}x</b></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600">
|
||||
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"></path></svg>
|
||||
<span>金币获取 <b>{{ $vip->jjb_multiplier }}x</b></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600">
|
||||
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"></path></svg>
|
||||
<span>专属入场特效 & 横幅</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
@if ($vip->price > 0 && $vipPaymentEnabled)
|
||||
<form action="{{ route('vip.payment.store') }}" method="POST"
|
||||
onsubmit="return confirm('确认支付 {{ $vip->price }} 元购买【{{ $vip->name }}】吗?');">
|
||||
@csrf
|
||||
<input type="hidden" name="vip_level_id" value="{{ $vip->id }}">
|
||||
<button type="submit" class="w-full py-3 rounded-2xl bg-indigo-600 hover:bg-indigo-700 text-white font-bold transition shadow-sm">
|
||||
{{ $isCurrentVipLevel ? '立即续费' : '立即购买' }}
|
||||
</button>
|
||||
</form>
|
||||
@elseif ($vip->price > 0)
|
||||
<div class="w-full py-3 rounded-2xl bg-gray-100 text-gray-500 text-center font-bold">支付暂未开启</div>
|
||||
@else
|
||||
<div class="w-full py-3 rounded-2xl bg-emerald-100 text-emerald-700 text-center font-bold">请联系管理员开通</div>
|
||||
@endif
|
||||
</div>
|
||||
@if ($vipPaymentEnabled)
|
||||
<form action="{{ route('vip.payment.store') }}" method="POST" class="mt-6">
|
||||
@csrf
|
||||
<input type="hidden" name="vip_level_id" value="{{ $vip->id }}">
|
||||
<button type="submit"
|
||||
@if($isDisabled) disabled @endif
|
||||
class="w-full py-3.5 rounded-2xl {{ $btnColor }} font-bold text-sm transition-all duration-200 active:scale-[0.98] shadow-sm">
|
||||
{{ $btnText }}
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user