优化vip
This commit is contained in:
@@ -47,79 +47,101 @@ class VipCenterController extends Controller
|
||||
->where('status', 'paid')
|
||||
->sum('amount');
|
||||
|
||||
$data = [
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'is_vip' => $user->isVip(),
|
||||
'vip_name' => $user->vipName(),
|
||||
'hy_time' => $user->hy_time?->format('Y-m-d H:i'),
|
||||
'vip_level_id' => $user->vip_level_id,
|
||||
'can_customize' => $user->canCustomizeVipPresence(),
|
||||
'custom_join_message' => $user->custom_join_message,
|
||||
'custom_leave_message' => $user->custom_leave_message,
|
||||
'custom_join_effect' => $user->custom_join_effect,
|
||||
'custom_leave_effect' => $user->custom_leave_effect,
|
||||
'vip_level' => $user->vipLevel ? [
|
||||
'id' => $user->vipLevel->id,
|
||||
'name' => $user->vipLevel->name,
|
||||
'icon' => $user->vipLevel->icon,
|
||||
'color' => $user->vipLevel->color,
|
||||
'join_effect' => $user->vipLevel->joinEffectKey(),
|
||||
'join_banner' => $user->vipLevel->joinBannerStyleKey(),
|
||||
'leave_effect' => $user->vipLevel->leaveEffectKey(),
|
||||
'leave_banner' => $user->vipLevel->leaveBannerStyleKey(),
|
||||
'join_templates' => $user->vipLevel->join_templates_array,
|
||||
'leave_templates' => $user->vipLevel->leave_templates_array,
|
||||
] : null,
|
||||
],
|
||||
'vipLevels' => $vipLevels->map(function ($vip) {
|
||||
return [
|
||||
'id' => $vip->id,
|
||||
'name' => $vip->name,
|
||||
'icon' => $vip->icon,
|
||||
'color' => $vip->color,
|
||||
'price' => $vip->price,
|
||||
'duration_days' => $vip->duration_days,
|
||||
'exp_multiplier' => $vip->exp_multiplier,
|
||||
'jjb_multiplier' => $vip->jjb_multiplier,
|
||||
'description' => $vip->description,
|
||||
];
|
||||
}),
|
||||
'paymentLogs' => $paymentLogs->items(),
|
||||
'vipPaymentEnabled' => Sysparam::getValue('vip_payment_enabled', '0') === '1',
|
||||
'paidOrders' => $paidOrders,
|
||||
'totalAmount' => $totalAmount,
|
||||
'effectOptions' => [
|
||||
'none' => '无特效',
|
||||
'fireworks' => '烟花',
|
||||
'rain' => '下雨',
|
||||
'lightning' => '闪电',
|
||||
'snow' => '下雪',
|
||||
'sakura' => '樱花飘落',
|
||||
'meteors' => '流星',
|
||||
'gold-rain' => '金币雨',
|
||||
'hearts' => '爱心飘落',
|
||||
'confetti' => '彩带庆典',
|
||||
'fireflies' => '萤火虫',
|
||||
],
|
||||
'bannerStyleOptions' => [
|
||||
'aurora' => '鎏光星幕',
|
||||
'storm' => '雷霆风暴',
|
||||
'royal' => '王者金辉',
|
||||
'cosmic' => '星穹幻彩',
|
||||
'farewell' => '告别暮光',
|
||||
],
|
||||
$vipPaymentEnabled = Sysparam::getValue('vip_payment_enabled', '0') === '1';
|
||||
|
||||
$effectOptions = [
|
||||
'none' => '无特效',
|
||||
'fireworks' => '烟花',
|
||||
'rain' => '下雨',
|
||||
'lightning' => '闪电',
|
||||
'snow' => '下雪',
|
||||
'sakura' => '樱花飘落',
|
||||
'meteors' => '流星',
|
||||
'gold-rain' => '金币雨',
|
||||
'hearts' => '爱心飘落',
|
||||
'confetti' => '彩带庆典',
|
||||
'fireflies' => '萤火虫',
|
||||
];
|
||||
|
||||
$bannerStyleOptions = [
|
||||
'aurora' => '鎏光星幕',
|
||||
'storm' => '雷霆风暴',
|
||||
'royal' => '王者金辉',
|
||||
'cosmic' => '星穹幻彩',
|
||||
'farewell' => '告别暮光',
|
||||
];
|
||||
|
||||
if ($request->expectsJson()) {
|
||||
$data = [
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'is_vip' => $user->isVip(),
|
||||
'vip_name' => $user->vipName(),
|
||||
'hy_time' => $user->hy_time?->format('Y-m-d H:i'),
|
||||
'vip_level_id' => $user->vip_level_id,
|
||||
'can_customize' => $user->canCustomizeVipPresence(),
|
||||
'custom_join_message' => $user->custom_join_message,
|
||||
'custom_leave_message' => $user->custom_leave_message,
|
||||
'custom_join_effect' => $user->custom_join_effect,
|
||||
'custom_leave_effect' => $user->custom_leave_effect,
|
||||
'vip_level' => $user->vipLevel ? [
|
||||
'id' => $user->vipLevel->id,
|
||||
'name' => $user->vipLevel->name,
|
||||
'icon' => $user->vipLevel->icon,
|
||||
'color' => $user->vipLevel->color,
|
||||
'join_effect' => $user->vipLevel->joinEffectKey(),
|
||||
'join_banner' => $user->vipLevel->joinBannerStyleKey(),
|
||||
'leave_effect' => $user->vipLevel->leaveEffectKey(),
|
||||
'leave_banner' => $user->vipLevel->leaveBannerStyleKey(),
|
||||
'join_templates' => $user->vipLevel->join_templates_array,
|
||||
'leave_templates' => $user->vipLevel->leave_templates_array,
|
||||
] : null,
|
||||
],
|
||||
'vipLevels' => $vipLevels->map(function ($vip) use ($user) {
|
||||
$isCurrent = $user->isVip() && (int) $user->vip_level_id === (int) $vip->id;
|
||||
$isHigher = $user->isVip() ? $vip->isHigherThan($user->vip_level_id) : true;
|
||||
$isLower = $user->isVip() && ! $isCurrent && ! $isHigher;
|
||||
|
||||
return [
|
||||
'id' => $vip->id,
|
||||
'name' => $vip->name,
|
||||
'icon' => $vip->icon,
|
||||
'color' => $vip->color,
|
||||
'price' => (float) $vip->price,
|
||||
'upgrade_price' => $user->isVip() ? (float) $vip->getUpgradePrice($user->vip_level_id) : (float) $vip->price,
|
||||
'duration_days' => $vip->duration_days,
|
||||
'exp_multiplier' => $vip->exp_multiplier,
|
||||
'jjb_multiplier' => $vip->jjb_multiplier,
|
||||
'description' => $vip->description,
|
||||
'is_current' => $isCurrent,
|
||||
'is_higher' => $isHigher,
|
||||
'is_lower' => $isLower,
|
||||
];
|
||||
}), 'paymentLogs' => $paymentLogs->items(),
|
||||
'vipPaymentEnabled' => $vipPaymentEnabled,
|
||||
'paidOrders' => $paidOrders,
|
||||
'totalAmount' => $totalAmount,
|
||||
'effectOptions' => $effectOptions,
|
||||
'bannerStyleOptions' => $bannerStyleOptions,
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
return view('vip.center', $data);
|
||||
return view('vip.center', [
|
||||
'user' => $user,
|
||||
'vipLevels' => $vipLevels,
|
||||
'paymentLogs' => $paymentLogs,
|
||||
'vipPaymentEnabled' => $vipPaymentEnabled,
|
||||
'paidOrders' => $paidOrders,
|
||||
'totalAmount' => $totalAmount,
|
||||
'effectOptions' => $effectOptions,
|
||||
'bannerStyleOptions' => $bannerStyleOptions,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,36 @@ class CreateVipPaymentOrderRequest extends FormRequest
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置验证器实例。
|
||||
*
|
||||
* @param \Illuminate\Validation\Validator $validator
|
||||
* @return void
|
||||
*/
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
$user = $this->user();
|
||||
if (! $user || ! $user->isVip()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$vipLevelId = (int) $this->vip_level_id;
|
||||
$targetLevel = \App\Models\VipLevel::find($vipLevelId);
|
||||
|
||||
if (! $targetLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currentLevelId = (int) $user->vip_level_id;
|
||||
|
||||
// 逻辑:允许续费当前等级,或购买更高等级。禁止降级。
|
||||
if ($vipLevelId !== $currentLevelId && ! $targetLevel->isHigherThan($currentLevelId)) {
|
||||
$validator->errors()->add('vip_level_id', '当前仅支持续费同级会员或补差价升级到更高等级,暂不支持降级购买。');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中文错误提示
|
||||
*
|
||||
|
||||
@@ -13,6 +13,10 @@ namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* 聊天室入场、离场与会员喜报播报服务
|
||||
* 负责统一生成进退场文本,以及会员主题横幅和购买成功喜报的前端载荷。
|
||||
*/
|
||||
class RoomBroadcastService
|
||||
{
|
||||
/**
|
||||
@@ -183,4 +187,33 @@ class RoomBroadcastService
|
||||
'presence_icon' => (string) ($theme['icon'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建会员购买成功喜报的前端载荷。
|
||||
*
|
||||
* @return array<string, string|null>
|
||||
*/
|
||||
public function buildVipPurchasePayload(User $user): array
|
||||
{
|
||||
$theme = $this->vipPresenceService->buildPurchaseTheme($user);
|
||||
|
||||
if (empty($theme['enabled'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$text = trim((string) ($theme['text'] ?? ''));
|
||||
if ($text === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'presence_type' => 'purchase',
|
||||
'presence_text' => $text,
|
||||
'presence_color' => (string) ($theme['color'] ?? ''),
|
||||
'presence_effect' => $theme['effect'] ? (string) $theme['effect'] : null,
|
||||
'presence_banner_style' => (string) ($theme['banner_style'] ?? ''),
|
||||
'presence_level_name' => (string) ($theme['level_name'] ?? ''),
|
||||
'presence_icon' => (string) ($theme['icon'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Events\EffectBroadcast;
|
||||
use App\Events\MessageSent;
|
||||
use App\Models\User;
|
||||
use App\Models\VipLevel;
|
||||
use App\Models\VipPaymentOrder;
|
||||
@@ -14,6 +16,10 @@ use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* VIP 支付业务服务
|
||||
* 负责支付订单创建、支付回调幂等处理,以及会员开通成功后的聊天室喜报广播。
|
||||
*/
|
||||
class VipPaymentService
|
||||
{
|
||||
/**
|
||||
@@ -25,6 +31,8 @@ class VipPaymentService
|
||||
public function __construct(
|
||||
private readonly VipPaymentCenterClient $paymentCenterClient,
|
||||
private readonly VipService $vipService,
|
||||
private readonly ChatStateService $chatState,
|
||||
private readonly RoomBroadcastService $roomBroadcastService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -64,7 +72,7 @@ class VipPaymentService
|
||||
'vip_level_id' => $vipLevel->id,
|
||||
'status' => 'created',
|
||||
'amount' => $price,
|
||||
'subject' => ($isUpgrade ? '【升级】' : '购买') . ' VIP 会员 - ' . $vipLevel->name,
|
||||
'subject' => ($isUpgrade ? '【升级】' : '购买').' VIP 会员 - '.$vipLevel->name,
|
||||
'provider' => 'alipay',
|
||||
'vip_name' => $vipLevel->name,
|
||||
'vip_duration_days' => (int) $vipLevel->duration_days,
|
||||
@@ -173,7 +181,9 @@ class VipPaymentService
|
||||
*/
|
||||
public function markOrderAsPaid(VipPaymentOrder $vipPaymentOrder, array $payload, string $source = 'async'): VipPaymentOrder
|
||||
{
|
||||
return DB::transaction(function () use ($vipPaymentOrder, $payload, $source) {
|
||||
$shouldBroadcastVipCelebration = false;
|
||||
|
||||
$paidOrder = DB::transaction(function () use ($vipPaymentOrder, $payload, $source, &$shouldBroadcastVipCelebration) {
|
||||
$lockedOrder = VipPaymentOrder::query()
|
||||
->with(['user', 'vipLevel'])
|
||||
->lockForUpdate()
|
||||
@@ -208,19 +218,28 @@ class VipPaymentService
|
||||
if (! $lockedOrder->isVipOpened()) {
|
||||
// 只在首次成功支付时开通会员,防止重复回调导致会员时长重复叠加。
|
||||
$user = User::query()->lockForUpdate()->findOrFail($lockedOrder->user_id);
|
||||
|
||||
// 从 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(),
|
||||
]);
|
||||
|
||||
$shouldBroadcastVipCelebration = true;
|
||||
}
|
||||
|
||||
return $lockedOrder->fresh(['user', 'vipLevel']);
|
||||
});
|
||||
|
||||
if ($shouldBroadcastVipCelebration) {
|
||||
$this->broadcastVipPurchaseCelebration($paidOrder);
|
||||
}
|
||||
|
||||
return $paidOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,4 +249,51 @@ class VipPaymentService
|
||||
{
|
||||
return 'VPO'.date('YmdHis').random_int(1000, 9999);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向用户当前在线的聊天室广播 VIP 购买成功喜报与烟花特效。
|
||||
*
|
||||
* @param VipPaymentOrder $vipPaymentOrder 已完成开通的支付订单
|
||||
*/
|
||||
private function broadcastVipPurchaseCelebration(VipPaymentOrder $vipPaymentOrder): void
|
||||
{
|
||||
$user = User::query()->with('vipLevel')->find($vipPaymentOrder->user_id);
|
||||
|
||||
if (! $user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$purchasePayload = $this->roomBroadcastService->buildVipPurchasePayload($user);
|
||||
if (empty($purchasePayload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$roomIds = $this->chatState->getUserRooms($user->username);
|
||||
foreach ($roomIds as $roomId) {
|
||||
// 先把喜报写入房间历史,确保当前在线用户和后续短时间内进房的人都能看到。
|
||||
$celebrationMessage = [
|
||||
'id' => $this->chatState->nextMessageId($roomId),
|
||||
'room_id' => $roomId,
|
||||
'from_user' => '会员播报',
|
||||
'to_user' => '大家',
|
||||
'content' => sprintf(
|
||||
'<span style="color: %s; font-weight: bold;">%s</span>',
|
||||
$purchasePayload['presence_color'] ?: '#f59e0b',
|
||||
$purchasePayload['presence_text']
|
||||
),
|
||||
'is_secret' => false,
|
||||
'font_color' => $purchasePayload['presence_color'] ?: '#f59e0b',
|
||||
'action' => 'vip_presence',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
$celebrationMessage = array_merge($celebrationMessage, $purchasePayload);
|
||||
|
||||
$this->chatState->pushMessage($roomId, $celebrationMessage);
|
||||
broadcast(new MessageSent($roomId, $celebrationMessage));
|
||||
|
||||
// 购买成功固定播放烟花,和会员登录时的豪华表现保持一致。
|
||||
broadcast(new EffectBroadcast($roomId, 'fireworks', $user->username));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* 会员进退场与购买成功主题服务
|
||||
* 统一生成会员欢迎横幅、离场横幅与购买成功喜报所需的文本、颜色、特效和样式数据。
|
||||
*/
|
||||
class VipPresenceService
|
||||
{
|
||||
/**
|
||||
@@ -39,6 +43,45 @@ class VipPresenceService
|
||||
return $this->buildTheme($user, 'leave');
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建会员购买成功主题数据。
|
||||
*
|
||||
* @return array<string, string|null|bool>
|
||||
*/
|
||||
public function buildPurchaseTheme(User $user): array
|
||||
{
|
||||
$vipLevel = $user->vipLevel;
|
||||
|
||||
if (! $user->isVip() || ! $vipLevel) {
|
||||
return [
|
||||
'enabled' => false,
|
||||
'type' => 'purchase',
|
||||
'text' => null,
|
||||
'color' => null,
|
||||
'effect' => null,
|
||||
'banner_style' => null,
|
||||
'level_name' => null,
|
||||
'icon' => null,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => true,
|
||||
'type' => 'purchase',
|
||||
'text' => sprintf(
|
||||
'恭喜 %s 成功开通【%s %s】,尊享 VIP 特权,大家掌声欢迎!',
|
||||
$user->username,
|
||||
$vipLevel->icon ?: '👑',
|
||||
$vipLevel->name
|
||||
),
|
||||
'color' => $vipLevel->color ?: '#f59e0b',
|
||||
'effect' => 'fireworks',
|
||||
'banner_style' => $vipLevel->joinBannerStyleKey(),
|
||||
'level_name' => $vipLevel->name,
|
||||
'icon' => $vipLevel->icon,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一构建会员进场或离场的主题数据。
|
||||
*
|
||||
|
||||
@@ -1648,7 +1648,34 @@ async function generateWechatBindCode() {
|
||||
// 会员等级
|
||||
const grid = document.getElementById('vip-levels-grid');
|
||||
grid.innerHTML = d.vipLevels.map(v => {
|
||||
const isCurrent = d.user.vip_level_id === v.id && d.user.is_vip;
|
||||
const isCurrent = v.is_current;
|
||||
const isHigher = v.is_higher;
|
||||
const isLower = v.is_lower;
|
||||
|
||||
let btnText = '立即购买';
|
||||
let btnColor = '#1e293b';
|
||||
let btnTextColor = '#fff';
|
||||
let priceToDisplay = v.price;
|
||||
let isDisabled = !d.vipPaymentEnabled;
|
||||
let showUpgradeInfo = false;
|
||||
|
||||
if (isCurrent) {
|
||||
btnText = '立即续费';
|
||||
btnColor = '#f59e0b';
|
||||
btnTextColor = '#fff';
|
||||
} else if (isHigher && d.user.is_vip) {
|
||||
btnText = '补差价升级';
|
||||
btnColor = '#4f46e5';
|
||||
btnTextColor = '#fff';
|
||||
priceToDisplay = v.upgrade_price;
|
||||
showUpgradeInfo = true;
|
||||
} else if (isLower) {
|
||||
btnText = '无法降级';
|
||||
btnColor = '#f1f5f9';
|
||||
btnTextColor = '#94a3b8';
|
||||
isDisabled = true;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="vip-level-card ${isCurrent ? 'current' : ''}">
|
||||
${isCurrent ? '<span class="vip-level-badge">当前档位</span>' : ''}
|
||||
@@ -1663,11 +1690,15 @@ async function generateWechatBindCode() {
|
||||
<div class="vip-feature-item"><span class="vip-feature-icon">✓</span> 专属入场特效 & 横幅</div>
|
||||
</div>
|
||||
<div style="margin-top:auto; padding-top:10px;">
|
||||
<div style="font-size:18px; font-weight:900; color:#e11d48; margin-bottom:10px;">¥${Number(v.price).toFixed(2)} <span style="font-size:11px; font-weight:normal; color:#94a3b8;">/ ${v.duration_days}天</span></div>
|
||||
<button onclick="buyVip(${v.id})" ${!d.vipPaymentEnabled ? 'disabled' : ''}
|
||||
<div style="font-size:18px; font-weight:900; color:#e11d48; margin-bottom:5px;">
|
||||
¥${Number(priceToDisplay).toFixed(2)}
|
||||
<span style="font-size:11px; font-weight:normal; color:#94a3b8;">/ ${v.duration_days}天</span>
|
||||
</div>
|
||||
${showUpgradeInfo ? `<div style="font-size:10px; color:#4f46e5; font-weight:bold; margin-bottom:8px;">已省 ¥${(v.price - v.upgrade_price).toFixed(2)}</div>` : ''}
|
||||
<button onclick="buyVip(${v.id})" ${isDisabled ? 'disabled' : ''}
|
||||
style="width:100%; border:none; border-radius:8px; padding:10px; font-size:13px; font-weight:bold; cursor:pointer; transition:all .2s;
|
||||
background:${isCurrent ? '#fef3c7' : '#1e293b'}; color:${isCurrent ? '#b45309' : '#fff'};">
|
||||
${!d.vipPaymentEnabled ? '支付暂未开启' : (isCurrent ? '立即续费' : '立即购买')}
|
||||
background:${btnColor}; color:${btnTextColor}; ${isDisabled ? 'cursor:not-allowed;' : ''}">
|
||||
${!d.vipPaymentEnabled && !isLower ? '支付暂未开启' : btnText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1769,9 +1800,11 @@ async function generateWechatBindCode() {
|
||||
|
||||
window.buyVip = function(levelId) {
|
||||
// 这里我们模拟提交表单,因为支付逻辑通常需要页面跳转
|
||||
// 修改为在新窗口打开支付,避免聊天室页面丢失
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '{{ route('vip.payment.store') }}';
|
||||
form.target = '_blank'; // 新窗口打开支付
|
||||
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
@@ -1787,6 +1820,13 @@ async function generateWechatBindCode() {
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
document.body.removeChild(form);
|
||||
|
||||
// 提交后关闭弹窗并提示用户
|
||||
closeVipModal();
|
||||
if (window.chatDialog) {
|
||||
window.chatDialog.alert('正在为您前往支付中心,请在新页面完成支付。', '支付提示', '#3b82f6');
|
||||
}
|
||||
};
|
||||
|
||||
window.saveVipPresenceSettings = async function() {
|
||||
|
||||
@@ -132,6 +132,9 @@
|
||||
}
|
||||
|
||||
const styleConfig = getVipPresenceStyleConfig(payload.presence_banner_style, payload.presence_color);
|
||||
const bannerTypeLabel = payload.presence_type === 'leave'
|
||||
? '离场提示'
|
||||
: (payload.presence_type === 'purchase' ? '开通喜报' : '闪耀登场');
|
||||
const banner = document.createElement('div');
|
||||
banner.id = 'vip-presence-banner';
|
||||
banner.className = 'vip-presence-banner';
|
||||
@@ -141,7 +144,7 @@
|
||||
<div class="vip-presence-banner__meta">
|
||||
<span class="vip-presence-banner__icon">${escapeHtml(payload.presence_icon || '👑')}</span>
|
||||
<span class="vip-presence-banner__level">${escapeHtml(payload.presence_level_name || '尊贵会员')}</span>
|
||||
<span class="vip-presence-banner__type">${payload.presence_type === 'leave' ? '离场提示' : '闪耀登场'}</span>
|
||||
<span class="vip-presence-banner__type">${bannerTypeLabel}</span>
|
||||
</div>
|
||||
<div class="vip-presence-banner__text" style="color:${styleConfig.accent};">${escapePresenceText(payload.presence_text)}</div>
|
||||
</div>
|
||||
@@ -783,7 +786,9 @@
|
||||
|
||||
const icon = escapeHtml(msg.presence_icon || '👑');
|
||||
const levelName = escapeHtml(msg.presence_level_name || '尊贵会员');
|
||||
const typeLabel = msg.presence_type === 'leave' ? '华丽离场' : '荣耀入场';
|
||||
const typeLabel = msg.presence_type === 'leave'
|
||||
? '华丽离场'
|
||||
: (msg.presence_type === 'purchase' ? '荣耀开通' : '荣耀入场');
|
||||
const safeText = escapePresenceText(msg.presence_text || '');
|
||||
|
||||
html = `
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
</div>
|
||||
|
||||
@if ($vipPaymentEnabled)
|
||||
<form action="{{ route('vip.payment.store') }}" method="POST" class="mt-6">
|
||||
<form action="{{ route('vip.payment.store') }}" method="POST" class="mt-6" target="_blank">
|
||||
@csrf
|
||||
<input type="hidden" name="vip_level_id" value="{{ $vip->id }}">
|
||||
<button type="submit"
|
||||
|
||||
@@ -13,12 +13,26 @@ use App\Models\VipLevel;
|
||||
use App\Models\VipPaymentOrder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* VIP 支付集成功能测试
|
||||
* 覆盖创建订单、异步开通会员以及聊天室内购买成功喜报等关键链路。
|
||||
*/
|
||||
class VipPaymentIntegrationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* 每个测试前清空 Redis,避免聊天室在线状态与消息缓存互相污染。
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Redis::flushall();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试用户可以发起 VIP 支付并跳转到支付中心页面
|
||||
*/
|
||||
@@ -140,30 +154,19 @@ class VipPaymentIntegrationTest extends TestCase
|
||||
*/
|
||||
public function test_vip_center_only_shows_current_user_records(): void
|
||||
{
|
||||
$currentUser = User::factory()->create(['username' => 'current-user']);
|
||||
$otherUser = User::factory()->create(['username' => 'other-user']);
|
||||
$vipLevel = VipLevel::factory()->create([
|
||||
'name' => '星耀会员',
|
||||
'price' => 66,
|
||||
'duration_days' => 30,
|
||||
]);
|
||||
$this->seedVipPaymentConfig();
|
||||
|
||||
$currentUser = User::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
|
||||
VipPaymentOrder::factory()->create([
|
||||
'user_id' => $currentUser->id,
|
||||
'vip_level_id' => $vipLevel->id,
|
||||
'vip_name' => $vipLevel->name,
|
||||
'status' => 'paid',
|
||||
'order_no' => 'VPO_CURRENT_001',
|
||||
'merchant_order_no' => 'MER_CURRENT_001',
|
||||
]);
|
||||
|
||||
VipPaymentOrder::factory()->create([
|
||||
'user_id' => $otherUser->id,
|
||||
'vip_level_id' => $vipLevel->id,
|
||||
'vip_name' => $vipLevel->name,
|
||||
'status' => 'paid',
|
||||
'order_no' => 'VPO_OTHER_001',
|
||||
'merchant_order_no' => 'MER_OTHER_001',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($currentUser)->get(route('vip.center'));
|
||||
@@ -179,6 +182,8 @@ class VipPaymentIntegrationTest extends TestCase
|
||||
*/
|
||||
public function test_vip_member_can_update_custom_presence_messages(): void
|
||||
{
|
||||
$this->seedVipPaymentConfig();
|
||||
|
||||
$vipLevel = VipLevel::factory()->create([
|
||||
'allow_custom_messages' => true,
|
||||
]);
|
||||
@@ -201,10 +206,12 @@ class VipPaymentIntegrationTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试未开通该权限的用户不能保存自定义欢迎语和离开语。
|
||||
* 测试普通用户或不允许自定义的会员无法保存专属语句。
|
||||
*/
|
||||
public function test_non_customizable_vip_member_cannot_update_custom_presence_messages(): void
|
||||
{
|
||||
$this->seedVipPaymentConfig();
|
||||
|
||||
$vipLevel = VipLevel::factory()->create([
|
||||
'allow_custom_messages' => false,
|
||||
]);
|
||||
@@ -226,15 +233,15 @@ class VipPaymentIntegrationTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入测试所需的支付中心配置
|
||||
* 初始化支付中心模拟配置
|
||||
*/
|
||||
private function seedVipPaymentConfig(): void
|
||||
{
|
||||
// 这些配置与聊天室后台保存的数据结构保持一致,方便直接复用真实业务代码。
|
||||
Sysparam::query()->updateOrCreate(['alias' => 'vip_payment_enabled'], ['body' => '1']);
|
||||
Sysparam::query()->updateOrCreate(['alias' => 'vip_payment_base_url'], ['body' => 'https://novalink.test']);
|
||||
Sysparam::query()->updateOrCreate(['alias' => 'vip_payment_app_key'], ['body' => 'chatroom-app']);
|
||||
Sysparam::query()->updateOrCreate(['alias' => 'vip_payment_app_secret'], ['body' => 'chatroom-secret']);
|
||||
Sysparam::query()->updateOrCreate(['alias' => 'vip_payment_timeout'], ['body' => '10']);
|
||||
Sysparam::updateOrCreate(['alias' => 'vip_payment_enabled'], ['body' => '1']);
|
||||
Sysparam::clearCache('vip_payment_enabled');
|
||||
|
||||
config(['services.novalink_payment_center.client_id' => 'chatroom-app']);
|
||||
config(['services.novalink_payment_center.client_secret' => 'chatroom-secret']);
|
||||
config(['services.novalink_payment_center.base_url' => 'https://novalink.test/api/open/v1']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user