feat: 任命/撤销通知系统 + 用户名片UI优化

- 任命/撤销事件增加 type 字段区分类型
- 任命:全屏礼花 + 紫色弹窗 + 紫色系统消息
- 撤销:灰色弹窗 + 灰色系统消息,无礼花
- 消息分发:操作者/被操作者显示在私聊面板,其他人显示在公屏
- 系统消息加随机鼓励语(各5条轮换)
- ChatStateService 修复 Redis key 前缀扫描问题(getAllActiveRoomIds)
- 用户名片折叠优化:管理员视野、职务履历均可折叠
- 管理操作 + 职务操作合并为「🔧 管理操作」折叠区
- 悄悄话改为「🎁 送礼物」按钮,礼物面板内联展开
This commit is contained in:
2026-02-28 23:44:38 +08:00
parent a599047cf0
commit 5f30220609
80 changed files with 8579 additions and 473 deletions
+74 -7
View File
@@ -53,15 +53,18 @@ class ChatStateService
public function getUserRooms(string $username): array
{
$rooms = [];
$prefix = config('database.redis.options.prefix', '');
$cursor = '0';
do {
[$cursor, $keys] = Redis::scan($cursor, ['match' => 'room:*:users', 'count' => 100]);
foreach ($keys ?? [] as $key) {
if (Redis::hexists($key, $username)) {
// 从 key "room:123:users" 中提取房间 ID
preg_match('/room:(\d+):users/', $key, $matches);
if (isset($matches[1])) {
$rooms[] = (int) $matches[1];
// scan 带前缀通配,返回的 key 也带前缀
[$cursor, $keys] = Redis::scan($cursor, ['match' => $prefix.'room:*:users', 'count' => 100]);
foreach ($keys ?? [] as $fullKey) {
// 去掉前缀得到 Laravel Redis Facade 认识的短少 key
$shortKey = $prefix ? ltrim(substr($fullKey, strlen($prefix)), '') : $fullKey;
if (Redis::hexists($shortKey, $username)) {
preg_match('/room:(\d+):users/', $shortKey, $m);
if (isset($m[1])) {
$rooms[] = (int) $m[1];
}
}
}
@@ -88,6 +91,34 @@ class ChatStateService
return $result;
}
/**
* 扫描 Redis,返回当前所有有在线用户的房间 ID 数组(用于全局广播)。
*
* @return array<int>
*/
public function getAllActiveRoomIds(): array
{
$roomIds = [];
$prefix = config('database.redis.options.prefix', '');
$cursor = '0';
do {
// scan 带前缀通配,返回的 key 也带前缀
[$cursor, $keys] = Redis::scan($cursor, ['match' => $prefix.'room:*:users', 'count' => 100]);
foreach ($keys ?? [] as $fullKey) {
$shortKey = $prefix ? substr($fullKey, strlen($prefix)) : $fullKey;
// 只有 hash 非空(有在线用户)才算活跃房间
if (Redis::hlen($shortKey) > 0) {
preg_match('/room:(\d+):users/', $shortKey, $m);
if (isset($m[1])) {
$roomIds[] = (int) $m[1];
}
}
}
} while ($cursor !== '0');
return array_unique($roomIds);
}
/**
* 将一条新发言推入 Redis 列表,并限制最大保留数量,防止内存泄漏。
*
@@ -115,6 +146,42 @@ class ChatStateService
Redis::del($key);
}
/**
* 清除指定房间内,关于某个用户的旧欢迎消息(支持普通人、管理员、新人)。
* 保证聊天记录里只保留最新的一条,解决频繁进出造成的刷屏问题。
*
* @param int $roomId 房间ID
* @param string $username 用户名
*/
public function removeOldWelcomeMessages(int $roomId, string $username): void
{
$key = "room:{$roomId}:messages";
$messages = Redis::lrange($key, 0, -1);
if (empty($messages)) {
return;
}
$filtered = [];
foreach ($messages as $msgJson) {
$msg = json_decode($msgJson, true);
// 只要消息里带了 welcome_user 且等于当前用户,就抛弃这条旧的
if ($msg && isset($msg['welcome_user']) && $msg['welcome_user'] === $username) {
continue;
}
$filtered[] = $msgJson;
}
// 重新写回 Redis(如果发生了过滤)
if (count($filtered) !== count($messages)) {
Redis::del($key);
if (! empty($filtered)) {
Redis::rpush($key, ...$filtered);
}
}
}
/**
* 获取指定房间的新发言记录。
* 在高频长轮询或前端断线重连拉取时使用。