功能:window.chatBanner 全局大卡片公共组件
前端: - window.chatBanner.show(options) 全局 API,完全自定义: icon/title/name/body/sub/gradient/titleColor/autoClose/buttons - window.chatBanner.close(id) 关闭指定 banner - showFriendBanner / showAppointmentBanner 均改用 chatBanner 实现 - setupBannerNotification() 监听私有+房间频道的 BannerNotification 事件 后端: - BannerNotification 事件(ShouldBroadcastNow),支持 user/room 双目标 - BannerBroadcastController(仅超级管理员路由,三层中间件保护) - 内容字段 strip_tags 净化防 XSS,按钮 action 白名单校验 安全: - window.chatBanner.show() 被人控制台调用只影响自己,无法推给他人 - HTTP 入口 POST /admin/banner/broadcast 仅超管可访问
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:通用大卡片通知广播事件
|
||||
*
|
||||
* 可向指定用户(私有频道)或房间所有人(Presence 频道)推送全屏大卡片通知。
|
||||
* 前端通过 window.chatBanner.show(options) 渲染,支持完全自定义。
|
||||
*
|
||||
* 使用示例(后端):
|
||||
*
|
||||
* // 推给单个用户
|
||||
* broadcast(new BannerNotification(
|
||||
* target: 'user',
|
||||
* targetId: 'lkddi',
|
||||
* options: [
|
||||
* 'icon' => '💚📩',
|
||||
* 'title' => '好友申请',
|
||||
* 'name' => 'lkddi1',
|
||||
* 'body' => '将你加为好友了!',
|
||||
* 'gradient' => ['#1e3a5f', '#1d4ed8', '#0891b2'],
|
||||
* 'autoClose' => 0,
|
||||
* 'buttons' => [
|
||||
* ['label' => '➕ 回加好友', 'color' => '#10b981', 'action' => 'add_friend', 'actionData' => 'lkddi1'],
|
||||
* ['label' => '稍后再说', 'color' => 'rgba(255,255,255,0.15)', 'action' => 'close'],
|
||||
* ],
|
||||
* ]
|
||||
* ));
|
||||
*
|
||||
* // 推给整个房间
|
||||
* broadcast(new BannerNotification(target: 'room', targetId: 1, options: [...] ));
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class BannerNotification implements ShouldBroadcastNow
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* 构造通用大卡片通知事件。
|
||||
*
|
||||
* @param string $target 推送目标类型:'user'(私有频道)| 'room'(房间全员)
|
||||
* @param string|int $targetId 目标 ID:用户名(user)或 房间 ID(room)
|
||||
* @param array<string, mixed> $options 前端 chatBanner.show() 选项(详见文件顶部注释)
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $target,
|
||||
public readonly string|int $targetId,
|
||||
public readonly array $options = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 根据 $target 决定广播到私有频道还是 Presence 频道。
|
||||
*/
|
||||
public function broadcastOn(): Channel
|
||||
{
|
||||
return match ($this->target) {
|
||||
'user' => new PrivateChannel('user.'.$this->targetId),
|
||||
'room' => new PresenceChannel('room.'.$this->targetId),
|
||||
default => new PrivateChannel('user.'.$this->targetId),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定广播事件名称,供前端 .listen('.BannerNotification') 匹配。
|
||||
*/
|
||||
public function broadcastAs(): string
|
||||
{
|
||||
return 'BannerNotification';
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播负载:传递完整的 options 给前端渲染。
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'target' => $this->target,
|
||||
'target_id' => $this->targetId,
|
||||
'options' => $this->options,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:管理员大卡片通知广播控制器
|
||||
*
|
||||
* 仅超级管理员(chat.level:super 中间件保护)可调用此接口,
|
||||
* 通过 BannerNotification 事件向指定用户或房间推送自定义大卡通知。
|
||||
*
|
||||
* 安全保证:
|
||||
* - 路由被 ['chat.auth', 'chat.has_position', 'chat.level:super'] 三层中间件保护
|
||||
* - 普通用户无权访问此接口,无法伪造对他人的广播
|
||||
* - options 中的用户输入字段在后端经过 strip_tags 清洗
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Events\BannerNotification;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BannerBroadcastController extends Controller
|
||||
{
|
||||
/**
|
||||
* 向指定目标广播大卡片通知。
|
||||
*
|
||||
* 请求参数:
|
||||
* - target: 'user' | 'room'
|
||||
* - target_id: 用户名 或 房间 ID
|
||||
* - options: 与 window.chatBanner.show() 参数相同的对象
|
||||
* - icon, title, name, body, sub, gradient(array), titleColor, autoClose, buttons(array)
|
||||
*/
|
||||
public function send(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'target' => ['required', 'in:user,room'],
|
||||
'target_id' => ['required'],
|
||||
'options' => ['required', 'array'],
|
||||
'options.icon' => ['nullable', 'string', 'max:20'],
|
||||
'options.title' => ['nullable', 'string', 'max:50'],
|
||||
'options.name' => ['nullable', 'string', 'max:100'],
|
||||
'options.body' => ['nullable', 'string', 'max:500'],
|
||||
'options.sub' => ['nullable', 'string', 'max:200'],
|
||||
'options.gradient' => ['nullable', 'array', 'max:5'],
|
||||
'options.titleColor' => ['nullable', 'string', 'max:30'],
|
||||
'options.autoClose' => ['nullable', 'integer', 'min:0', 'max:30000'],
|
||||
'options.buttons' => ['nullable', 'array', 'max:4'],
|
||||
]);
|
||||
|
||||
// 对可能包含用户输入的字段进行 HTML 净化(防 XSS)
|
||||
$opts = $validated['options'];
|
||||
foreach (['title', 'name', 'body', 'sub'] as $field) {
|
||||
if (isset($opts[$field])) {
|
||||
$opts[$field] = strip_tags($opts[$field], '<b><strong><em><span><br>');
|
||||
}
|
||||
}
|
||||
// 按钮 label 不允许 HTML
|
||||
if (! empty($opts['buttons'])) {
|
||||
$opts['buttons'] = array_map(function ($btn) {
|
||||
$btn['label'] = strip_tags($btn['label'] ?? '');
|
||||
$btn['color'] = preg_replace('/[^a-z0-9#(),\s.%rgba\/]/i', '', $btn['color'] ?? '#10b981');
|
||||
// action 只允许预定义值,防止注入任意 JS
|
||||
$btn['action'] = in_array($btn['action'] ?? '', ['close', 'add_friend', 'remove_friend', 'link'])
|
||||
? $btn['action'] : 'close';
|
||||
|
||||
return $btn;
|
||||
}, $opts['buttons']);
|
||||
}
|
||||
|
||||
broadcast(new BannerNotification(
|
||||
target: $validated['target'],
|
||||
targetId: $validated['target_id'],
|
||||
options: $opts,
|
||||
));
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => '广播已发送']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user