Add VIP presence themes and custom greetings
This commit is contained in:
@@ -20,6 +20,32 @@ use Illuminate\View\View;
|
||||
|
||||
class VipController extends Controller
|
||||
{
|
||||
/**
|
||||
* 会员主题支持的特效下拉选项。
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private const EFFECT_LABELS = [
|
||||
'none' => '无特效',
|
||||
'fireworks' => '烟花',
|
||||
'rain' => '下雨',
|
||||
'lightning' => '闪电',
|
||||
'snow' => '下雪',
|
||||
];
|
||||
|
||||
/**
|
||||
* 会员主题支持的横幅风格下拉选项。
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private const BANNER_STYLE_LABELS = [
|
||||
'aurora' => '鎏光星幕',
|
||||
'storm' => '雷霆风暴',
|
||||
'royal' => '王者金辉',
|
||||
'cosmic' => '星穹幻彩',
|
||||
'farewell' => '告别暮光',
|
||||
];
|
||||
|
||||
/**
|
||||
* 会员等级管理列表页
|
||||
*/
|
||||
@@ -27,7 +53,11 @@ class VipController extends Controller
|
||||
{
|
||||
$levels = VipLevel::orderBy('sort_order')->get();
|
||||
|
||||
return view('admin.vip.index', compact('levels'));
|
||||
return view('admin.vip.index', [
|
||||
'levels' => $levels,
|
||||
'effectOptions' => self::EFFECT_LABELS,
|
||||
'bannerStyleOptions' => self::BANNER_STYLE_LABELS,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,22 +65,7 @@ class VipController extends Controller
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'icon' => 'required|string|max:20',
|
||||
'color' => 'required|string|max:10',
|
||||
'exp_multiplier' => 'required|numeric|min:1|max:99',
|
||||
'jjb_multiplier' => 'required|numeric|min:1|max:99',
|
||||
'sort_order' => 'required|integer|min:0',
|
||||
'price' => 'required|integer|min:0',
|
||||
'duration_days' => 'required|integer|min:0',
|
||||
'join_templates' => 'nullable|string',
|
||||
'leave_templates' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// 将文本框的多行模板转为 JSON 数组
|
||||
$data['join_templates'] = $this->textToJson($data['join_templates'] ?? '');
|
||||
$data['leave_templates'] = $this->textToJson($data['leave_templates'] ?? '');
|
||||
$data = $this->validatedPayload($request);
|
||||
|
||||
VipLevel::create($data);
|
||||
|
||||
@@ -66,21 +81,7 @@ class VipController extends Controller
|
||||
{
|
||||
$level = $vip;
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'icon' => 'required|string|max:20',
|
||||
'color' => 'required|string|max:10',
|
||||
'exp_multiplier' => 'required|numeric|min:1|max:99',
|
||||
'jjb_multiplier' => 'required|numeric|min:1|max:99',
|
||||
'sort_order' => 'required|integer|min:0',
|
||||
'price' => 'required|integer|min:0',
|
||||
'duration_days' => 'required|integer|min:0',
|
||||
'join_templates' => 'nullable|string',
|
||||
'leave_templates' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$data['join_templates'] = $this->textToJson($data['join_templates'] ?? '');
|
||||
$data['leave_templates'] = $this->textToJson($data['leave_templates'] ?? '');
|
||||
$data = $this->validatedPayload($request);
|
||||
|
||||
$level->update($data);
|
||||
|
||||
@@ -119,4 +120,37 @@ class VipController extends Controller
|
||||
|
||||
return json_encode(array_values($lines), JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一整理后台提交的会员等级主题配置数据。
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function validatedPayload(Request $request): array
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'icon' => 'required|string|max:20',
|
||||
'color' => 'required|string|max:10',
|
||||
'exp_multiplier' => 'required|numeric|min:1|max:99',
|
||||
'jjb_multiplier' => 'required|numeric|min:1|max:99',
|
||||
'sort_order' => 'required|integer|min:0',
|
||||
'price' => 'required|integer|min:0',
|
||||
'duration_days' => 'required|integer|min:0',
|
||||
'join_templates' => 'nullable|string',
|
||||
'leave_templates' => 'nullable|string',
|
||||
'join_effect' => 'required|in:none,fireworks,rain,lightning,snow',
|
||||
'leave_effect' => 'required|in:none,fireworks,rain,lightning,snow',
|
||||
'join_banner_style' => 'required|in:aurora,storm,royal,cosmic,farewell',
|
||||
'leave_banner_style' => 'required|in:aurora,storm,royal,cosmic,farewell',
|
||||
'allow_custom_messages' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
// 将多行文本框内容转为 JSON 数组,便于后续随机抽取模板。
|
||||
$data['join_templates'] = $this->textToJson($data['join_templates'] ?? '');
|
||||
$data['leave_templates'] = $this->textToJson($data['leave_templates'] ?? '');
|
||||
$data['allow_custom_messages'] = $request->boolean('allow_custom_messages');
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ use Intervention\Image\ImageManager;
|
||||
|
||||
class ChatController extends Controller
|
||||
{
|
||||
/**
|
||||
* 构造聊天室核心控制器所需依赖。
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ChatStateService $chatState,
|
||||
private readonly MessageFilterService $filter,
|
||||
@@ -112,6 +115,7 @@ class ChatController extends Controller
|
||||
|
||||
// 3. 广播和初始化欢迎(仅限初次进入)
|
||||
$newbieEffect = null;
|
||||
$initialPresenceTheme = null;
|
||||
|
||||
if (! $isAlreadyInRoom) {
|
||||
// 广播 UserJoined 事件,通知房间内的其他人
|
||||
@@ -176,6 +180,7 @@ class ChatController extends Controller
|
||||
} else {
|
||||
// 非站长:生成通用播报(有职务 > 有VIP > 普通随机词)
|
||||
[$text, $color] = $this->broadcast->buildEntryBroadcast($user);
|
||||
$vipPresencePayload = $this->broadcast->buildVipPresencePayload($user, 'join');
|
||||
|
||||
$generalWelcomeMsg = [
|
||||
'id' => $this->chatState->nextMessageId($id),
|
||||
@@ -185,13 +190,25 @@ class ChatController extends Controller
|
||||
'content' => "<span style=\"color: {$color}; font-weight: bold;\">{$text}</span>",
|
||||
'is_secret' => false,
|
||||
'font_color' => $color,
|
||||
'action' => 'system_welcome',
|
||||
'action' => empty($vipPresencePayload) ? 'system_welcome' : 'vip_presence',
|
||||
'welcome_user' => $user->username,
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
// 当会员等级带有专属主题时,把横幅与特效字段并入系统消息,供前端展示豪华进场效果。
|
||||
if (! empty($vipPresencePayload)) {
|
||||
$generalWelcomeMsg = array_merge($generalWelcomeMsg, $vipPresencePayload);
|
||||
$initialPresenceTheme = $vipPresencePayload;
|
||||
}
|
||||
|
||||
$this->chatState->pushMessage($id, $generalWelcomeMsg);
|
||||
// 修复:之前使用了 ->toOthers() 导致自己看不到自己的进场提示
|
||||
broadcast(new MessageSent($id, $generalWelcomeMsg));
|
||||
|
||||
// 会员专属特效需要单独广播给其他在线成员,自己则在页面初始化后本地补播。
|
||||
if (! empty($vipPresencePayload['presence_effect'])) {
|
||||
broadcast(new \App\Events\EffectBroadcast($id, $vipPresencePayload['presence_effect'], $user->username))->toOthers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +295,7 @@ class ChatController extends Controller
|
||||
'user' => $user,
|
||||
'weekEffect' => $this->shopService->getActiveWeekEffect($user),
|
||||
'newbieEffect' => $newbieEffect,
|
||||
'initialPresenceTheme' => $initialPresenceTheme,
|
||||
'historyMessages' => $historyMessages,
|
||||
'pendingProposal' => $pendingProposalData,
|
||||
'pendingDivorce' => $pendingDivorceData,
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
/**
|
||||
* 文件功能:前台会员中心控制器
|
||||
* 负责展示会员等级、权益说明、当前会员状态以及用户自己的购买记录
|
||||
* 负责展示会员等级、权益说明、当前会员状态、用户购买记录与会员个性化进退场设置
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\UpdateVipPresenceSettingsRequest;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\VipLevel;
|
||||
use App\Models\VipPaymentOrder;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
@@ -52,9 +54,50 @@ class VipCenterController extends Controller
|
||||
'vipPaymentEnabled' => Sysparam::getValue('vip_payment_enabled', '0') === '1',
|
||||
'paidOrders' => $paidOrders,
|
||||
'totalAmount' => $totalAmount,
|
||||
'effectOptions' => [
|
||||
'none' => '无特效',
|
||||
'fireworks' => '烟花',
|
||||
'rain' => '下雨',
|
||||
'lightning' => '闪电',
|
||||
'snow' => '下雪',
|
||||
],
|
||||
'bannerStyleOptions' => [
|
||||
'aurora' => '鎏光星幕',
|
||||
'storm' => '雷霆风暴',
|
||||
'royal' => '王者金辉',
|
||||
'cosmic' => '星穹幻彩',
|
||||
'farewell' => '告别暮光',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存会员个人自定义欢迎语与离开语。
|
||||
*/
|
||||
public function updatePresenceSettings(UpdateVipPresenceSettingsRequest $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
// 只有有效会员且当前等级允许自定义时,才允许保存专属语句。
|
||||
if (! $user->canCustomizeVipPresence()) {
|
||||
return redirect()
|
||||
->route('vip.center')
|
||||
->with('error', '当前会员等级暂不支持自定义欢迎语和离开语。');
|
||||
}
|
||||
|
||||
$data = $request->validated();
|
||||
|
||||
// 空字符串统一转成 null,避免数据库保存无意义空白值。
|
||||
$user->update([
|
||||
'custom_join_message' => $this->sanitizeNullableMessage($data['custom_join_message'] ?? null),
|
||||
'custom_leave_message' => $this->sanitizeNullableMessage($data['custom_leave_message'] ?? null),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('vip.center')
|
||||
->with('success', '会员专属欢迎语和离开语已保存。');
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建当前用户的购买记录分页数据
|
||||
*
|
||||
@@ -69,4 +112,14 @@ class VipCenterController extends Controller
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将可空文案统一整理为数据库可保存的字符串。
|
||||
*/
|
||||
private function sanitizeNullableMessage(?string $message): ?string
|
||||
{
|
||||
$message = trim((string) $message);
|
||||
|
||||
return $message === '' ? null : $message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:会员个性化欢迎语与离开语设置验证器
|
||||
* 负责校验会员中心提交的自定义进退场文案。
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateVipPresenceSettingsRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* 判断当前登录用户是否允许提交会员个性化设置。
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员个性化设置的验证规则。
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'custom_join_message' => ['nullable', 'string', 'max:255'],
|
||||
'custom_leave_message' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员个性化设置的中文错误提示。
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'custom_join_message.max' => '欢迎语最多只能填写 255 个字符。',
|
||||
'custom_leave_message.max' => '离开语最多只能填写 255 个字符。',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户离开聊天室后的异步清理与播报任务
|
||||
* 负责清理在线状态、关闭勤务日志,并根据会员/管理员身份发送不同的离场提示。
|
||||
*/
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\PositionDutyLog;
|
||||
@@ -19,12 +24,18 @@ class ProcessUserLeave implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* 构造离场处理任务实例。
|
||||
*/
|
||||
public function __construct(
|
||||
public int $roomId,
|
||||
public User $user,
|
||||
public float $leaveTime
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 执行离场任务:清理在线状态并广播离场消息。
|
||||
*/
|
||||
public function handle(ChatStateService $chatState, RoomBroadcastService $broadcast): void
|
||||
{
|
||||
// 获取该用户最后一次进入房间的时间
|
||||
@@ -66,6 +77,7 @@ class ProcessUserLeave implements ShouldQueue
|
||||
];
|
||||
} else {
|
||||
[$leaveText, $color] = $broadcast->buildLeaveBroadcast($this->user);
|
||||
$vipPresencePayload = $broadcast->buildVipPresencePayload($this->user, 'leave');
|
||||
$leaveMsg = [
|
||||
'id' => $chatState->nextMessageId($this->roomId),
|
||||
'room_id' => $this->roomId,
|
||||
@@ -74,16 +86,26 @@ class ProcessUserLeave implements ShouldQueue
|
||||
'content' => "<span style=\"color: {$color}; font-weight: bold;\">{$leaveText}</span>",
|
||||
'is_secret' => false,
|
||||
'font_color' => $color,
|
||||
'action' => 'system_welcome',
|
||||
'action' => empty($vipPresencePayload) ? 'system_welcome' : 'vip_presence',
|
||||
'welcome_user' => $this->user->username,
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
// 会员离场时,把横幅与特效信息挂到消息体,前端才能展示专属离场效果。
|
||||
if (! empty($vipPresencePayload)) {
|
||||
$leaveMsg = array_merge($leaveMsg, $vipPresencePayload);
|
||||
}
|
||||
}
|
||||
|
||||
// 将播报存入 Redis 历史及广播
|
||||
$chatState->pushMessage($this->roomId, $leaveMsg);
|
||||
broadcast(new \App\Events\UserLeft($this->roomId, $this->user->username))->toOthers();
|
||||
broadcast(new \App\Events\MessageSent($this->roomId, $leaveMsg))->toOthers();
|
||||
|
||||
// 离场特效单独发送给房间内仍在线的其他人,避免和消息播报逻辑耦死。
|
||||
if (! empty($leaveMsg['presence_effect'])) {
|
||||
broadcast(new \App\Events\EffectBroadcast($this->roomId, $leaveMsg['presence_effect'], $this->user->username))->toOthers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,6 +35,8 @@ class User extends Authenticatable
|
||||
'email',
|
||||
'sex',
|
||||
'sign',
|
||||
'custom_join_message',
|
||||
'custom_leave_message',
|
||||
'user_level',
|
||||
'inviter_id',
|
||||
'room_id',
|
||||
@@ -197,6 +199,18 @@ class User extends Authenticatable
|
||||
return $this->vipLevel?->icon ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户当前是否允许自定义会员进退场语句。
|
||||
*/
|
||||
public function canCustomizeVipPresence(): bool
|
||||
{
|
||||
if (! $this->isVip()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) $this->vipLevel?->allow_custom_messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联:当前用户的 VIP 购买订单记录
|
||||
*/
|
||||
|
||||
@@ -20,6 +20,32 @@ class VipLevel extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* 会员进退场支持的特效选项。
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public const EFFECT_OPTIONS = [
|
||||
'none',
|
||||
'fireworks',
|
||||
'rain',
|
||||
'lightning',
|
||||
'snow',
|
||||
];
|
||||
|
||||
/**
|
||||
* 会员进退场支持的横幅风格选项。
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public const BANNER_STYLE_OPTIONS = [
|
||||
'aurora',
|
||||
'storm',
|
||||
'royal',
|
||||
'cosmic',
|
||||
'farewell',
|
||||
];
|
||||
|
||||
/** @var string 表名 */
|
||||
protected $table = 'vip_levels';
|
||||
|
||||
@@ -32,6 +58,11 @@ class VipLevel extends Model
|
||||
'jjb_multiplier',
|
||||
'join_templates',
|
||||
'leave_templates',
|
||||
'join_effect',
|
||||
'leave_effect',
|
||||
'join_banner_style',
|
||||
'leave_banner_style',
|
||||
'allow_custom_messages',
|
||||
'sort_order',
|
||||
'price',
|
||||
'duration_days',
|
||||
@@ -44,6 +75,7 @@ class VipLevel extends Model
|
||||
'sort_order' => 'integer',
|
||||
'price' => 'integer',
|
||||
'duration_days' => 'integer',
|
||||
'allow_custom_messages' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -98,4 +130,44 @@ class VipLevel extends Model
|
||||
|
||||
return str_replace('{username}', $username, $template);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规范化后的入场特效键名。
|
||||
*/
|
||||
public function joinEffectKey(): string
|
||||
{
|
||||
return in_array($this->join_effect, self::EFFECT_OPTIONS, true)
|
||||
? (string) $this->join_effect
|
||||
: 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规范化后的离场特效键名。
|
||||
*/
|
||||
public function leaveEffectKey(): string
|
||||
{
|
||||
return in_array($this->leave_effect, self::EFFECT_OPTIONS, true)
|
||||
? (string) $this->leave_effect
|
||||
: 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规范化后的入场横幅风格键名。
|
||||
*/
|
||||
public function joinBannerStyleKey(): string
|
||||
{
|
||||
return in_array($this->join_banner_style, self::BANNER_STYLE_OPTIONS, true)
|
||||
? (string) $this->join_banner_style
|
||||
: 'aurora';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规范化后的离场横幅风格键名。
|
||||
*/
|
||||
public function leaveBannerStyleKey(): string
|
||||
{
|
||||
return in_array($this->leave_banner_style, self::BANNER_STYLE_OPTIONS, true)
|
||||
? (string) $this->leave_banner_style
|
||||
: 'farewell';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ use App\Models\User;
|
||||
class RoomBroadcastService
|
||||
{
|
||||
/**
|
||||
* 构造函数注入 VIP 服务(用于获取 VIP 专属入场/离场模板)
|
||||
* 构造函数注入会员进退场主题服务。
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly VipService $vipService,
|
||||
private readonly VipPresenceService $vipPresenceService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -43,11 +43,12 @@ class RoomBroadcastService
|
||||
|
||||
// 有 VIP:优先用专属进入模板,无模板则随机词加前缀
|
||||
if ($user->isVip() && $user->vipLevel) {
|
||||
$color = $user->vipLevel->color ?: '#f59e0b';
|
||||
$template = $this->vipService->getJoinMessage($user);
|
||||
$theme = $this->vipPresenceService->buildJoinTheme($user);
|
||||
$color = $theme['color'] ?: '#f59e0b';
|
||||
$template = $theme['text'];
|
||||
|
||||
if ($template) {
|
||||
return [$template, $color];
|
||||
return [(string) $template, $color];
|
||||
}
|
||||
|
||||
$text = '【'.$user->vipIcon().' '.$user->vipName().'】'.$this->randomWelcomeMsg($user);
|
||||
@@ -80,11 +81,12 @@ class RoomBroadcastService
|
||||
|
||||
// 有 VIP:优先用专属离场模板,无模板则随机词加前缀
|
||||
if ($user->isVip() && $user->vipLevel) {
|
||||
$color = $user->vipLevel->color ?: '#f59e0b';
|
||||
$template = $this->vipService->getLeaveMessage($user);
|
||||
$theme = $this->vipPresenceService->buildLeaveTheme($user);
|
||||
$color = $theme['color'] ?: '#f59e0b';
|
||||
$template = $theme['text'];
|
||||
|
||||
if ($template) {
|
||||
return [$template, $color];
|
||||
return [(string) $template, $color];
|
||||
}
|
||||
|
||||
$text = '【'.$user->vipIcon().' '.$user->vipName().'】'.$this->randomLeaveMsg($user);
|
||||
@@ -149,4 +151,36 @@ class RoomBroadcastService
|
||||
|
||||
return $templates[array_rand($templates)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建会员进退场横幅与特效的前端载荷。
|
||||
*
|
||||
* @param string $type join|leave
|
||||
* @return array<string, string|null>
|
||||
*/
|
||||
public function buildVipPresencePayload(User $user, string $type): array
|
||||
{
|
||||
$theme = $type === 'join'
|
||||
? $this->vipPresenceService->buildJoinTheme($user)
|
||||
: $this->vipPresenceService->buildLeaveTheme($user);
|
||||
|
||||
if (empty($theme['enabled'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$text = trim((string) ($theme['text'] ?? ''));
|
||||
if ($text === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'presence_type' => $type,
|
||||
'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'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:会员进退场主题服务
|
||||
* 统一解析会员等级主题、用户自定义文案、特效和横幅风格,
|
||||
* 避免聊天室进场与离场逻辑在多个位置重复拼装数据。
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class VipPresenceService
|
||||
{
|
||||
/**
|
||||
* 构造会员进退场主题服务实例。
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly VipService $vipService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 构建会员入场主题数据。
|
||||
*
|
||||
* @return array<string, string|null|bool>
|
||||
*/
|
||||
public function buildJoinTheme(User $user): array
|
||||
{
|
||||
return $this->buildTheme($user, 'join');
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建会员离场主题数据。
|
||||
*
|
||||
* @return array<string, string|null|bool>
|
||||
*/
|
||||
public function buildLeaveTheme(User $user): array
|
||||
{
|
||||
return $this->buildTheme($user, 'leave');
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一构建会员进场或离场的主题数据。
|
||||
*
|
||||
* @param string $type join|leave
|
||||
* @return array<string, string|null|bool>
|
||||
*/
|
||||
private function buildTheme(User $user, string $type): array
|
||||
{
|
||||
$vipLevel = $user->vipLevel;
|
||||
|
||||
if (! $user->isVip() || ! $vipLevel) {
|
||||
return [
|
||||
'enabled' => false,
|
||||
'type' => $type,
|
||||
'text' => null,
|
||||
'color' => null,
|
||||
'effect' => null,
|
||||
'banner_style' => null,
|
||||
'level_name' => null,
|
||||
'icon' => null,
|
||||
];
|
||||
}
|
||||
|
||||
// 先读取个人自定义文案,只有等级允许时才参与覆盖。
|
||||
$customMessage = $type === 'join'
|
||||
? $this->formatCustomMessage($user->custom_join_message, $user->username)
|
||||
: $this->formatCustomMessage($user->custom_leave_message, $user->username);
|
||||
|
||||
if (! $user->canCustomizeVipPresence()) {
|
||||
$customMessage = null;
|
||||
}
|
||||
|
||||
// 如果用户没有填写自定义文案,则回退到等级模板。
|
||||
$templateMessage = $type === 'join'
|
||||
? $this->vipService->getJoinMessage($user)
|
||||
: $this->vipService->getLeaveMessage($user);
|
||||
|
||||
return [
|
||||
'enabled' => true,
|
||||
'type' => $type,
|
||||
'text' => $customMessage ?: $templateMessage,
|
||||
'color' => $vipLevel->color ?: '#f59e0b',
|
||||
'effect' => $type === 'join' ? $this->normalizeEffect($vipLevel->joinEffectKey()) : $this->normalizeEffect($vipLevel->leaveEffectKey()),
|
||||
'banner_style' => $type === 'join' ? $vipLevel->joinBannerStyleKey() : $vipLevel->leaveBannerStyleKey(),
|
||||
'level_name' => $vipLevel->name,
|
||||
'icon' => $vipLevel->icon,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化用户自定义文案,支持 {username} 占位符。
|
||||
*/
|
||||
private function formatCustomMessage(?string $message, string $username): ?string
|
||||
{
|
||||
$message = trim((string) $message);
|
||||
|
||||
if ($message === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return str_replace('{username}', $username, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 none 这类占位值转换为 null,方便外部判断是否要播特效。
|
||||
*/
|
||||
private function normalizeEffect(string $effect): ?string
|
||||
{
|
||||
return $effect === 'none' ? null : $effect;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user