From 8bd1dae9e16522349e9906fed5b1fb7f8c332cf0 Mon Sep 17 00:00:00 2001 From: lkddi Date: Sat, 25 Apr 2026 02:24:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AD=98=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/AutoSaveExp.php | 10 +- app/Http/Controllers/ChatController.php | 6 ++ app/Services/ChatUserPresenceService.php | 44 +++++++- .../views/chat/partials/scripts.blade.php | 9 +- tests/Feature/AutoSaveExpCommandTest.php | 101 ++++++++++++++++++ tests/Feature/ChatControllerTest.php | 60 +++++++++++ 6 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 tests/Feature/AutoSaveExpCommandTest.php diff --git a/app/Console/Commands/AutoSaveExp.php b/app/Console/Commands/AutoSaveExp.php index 6ca0e2d..0f1b8ba 100644 --- a/app/Console/Commands/AutoSaveExp.php +++ b/app/Console/Commands/AutoSaveExp.php @@ -23,6 +23,7 @@ use App\Models\PositionDutyLog; use App\Models\Sysparam; use App\Models\User; use App\Services\ChatStateService; +use App\Services\ChatUserPresenceService; use App\Services\UserCurrencyService; use App\Services\VipService; use Illuminate\Console\Command; @@ -46,6 +47,7 @@ class AutoSaveExp extends Command */ public function __construct( private readonly ChatStateService $chatState, + private readonly ChatUserPresenceService $chatUserPresenceService, private readonly VipService $vipService, private readonly UserCurrencyService $currencyService, ) { @@ -164,7 +166,7 @@ class AutoSaveExp extends Command ); } $user->refresh(); // 刷新获取最新属性(service 已原子更新) - $user->load('activePosition.position'); // 确保职务及职位关联已加载 + $user->load(['activePosition.position.department', 'vipLevel']); // 存点通知需要展示部门、职务与会员身份。 // 3. 自动升降级逻辑 // - 有在职职务的用户:等级固定为职务对应等级,不随经验变化 @@ -229,9 +231,11 @@ class AutoSaveExp extends Command $jjbDisplay = $user->jjb ?? 0; $gainStr = ! empty($gainParts) ? ' 本次获得:'.implode(',', $gainParts) : ''; - // 格式:⏰ 自动存点 · LV.100 · 经验 10,468 · 金币 8,345 · 已满级 · 本次获得:经验+1,金币+3 + $identitySummary = $this->chatUserPresenceService->buildIdentitySummary($user); + + // 格式:⏰ 自动存点 · 部门 X · 职务 Y · 会员 Z · LV.100 · 经验 10,468 · 金币 8,345 · 已满级 · 本次获得:经验+1,金币+3 $statusTag = $user->user_level >= $superLevel ? ' · 已满级 ✓' : ''; - $content = "⏰ 自动存点 · LV.{$user->user_level} · 经验 {$user->exp_num} · 金币 {$jjbDisplay}{$statusTag}{$gainStr}"; + $content = "⏰ 自动存点 · {$identitySummary['inline']} · LV.{$user->user_level} · 经验 {$user->exp_num} · 金币 {$jjbDisplay}{$statusTag}{$gainStr}"; $noticeMsg = [ 'id' => $this->chatState->nextMessageId($roomId), diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php index b83c52c..ebc560d 100644 --- a/app/Http/Controllers/ChatController.php +++ b/app/Http/Controllers/ChatController.php @@ -634,6 +634,7 @@ class ChatController extends Controller } elseif ($user->user_level >= $superLevel) { $title = '管理员'; } + $identitySummary = $this->chatUserPresenceService->buildIdentitySummary($user); return response()->json([ 'status' => 'success', @@ -644,6 +645,11 @@ class ChatController extends Controller 'jjb_gain' => $actualJjbGain, 'user_level' => $user->user_level, 'title' => $title, + 'identity_summary' => $identitySummary['inline'], + 'department_name' => $identitySummary['department_name'], + 'position_name' => $identitySummary['position_name'], + 'vip_name' => $identitySummary['vip_name'], + 'vip_icon' => $identitySummary['vip_icon'], 'leveled_up' => $leveledUp, 'is_max_level' => $user->user_level >= $superLevel, 'auto_event' => $autoEvent ? $autoEvent->renderText($user->username) : null, diff --git a/app/Services/ChatUserPresenceService.php b/app/Services/ChatUserPresenceService.php index 7ba8869..fc1115a 100644 --- a/app/Services/ChatUserPresenceService.php +++ b/app/Services/ChatUserPresenceService.php @@ -11,6 +11,9 @@ use App\Models\Sysparam; use App\Models\User; use App\Support\ChatDailyStatusCatalog; +/** + * 类功能:统一生成聊天室在线用户与身份展示相关载荷。 + */ class ChatUserPresenceService { /** @@ -21,8 +24,8 @@ class ChatUserPresenceService public function build(User $user): array { $superLevel = (int) Sysparam::getValue('superlevel', '100'); - $activePosition = $user->activePosition; - $position = $activePosition?->position; + $user->loadMissing(['activePosition.position.department', 'vipLevel']); + $position = $user->activePosition?->position; $payload = [ 'id' => $user->id, 'user_id' => $user->id, @@ -64,6 +67,43 @@ class ChatUserPresenceService return $payload; } + /** + * 构建用户部门、职务与会员展示信息。 + * + * @return array{ + * department_name: string, + * position_icon: string, + * position_name: string, + * vip_icon: string, + * vip_name: string, + * vip_label: string, + * inline: string + * } + */ + public function buildIdentitySummary(User $user): array + { + $user->loadMissing(['activePosition.position.department', 'vipLevel']); + + $position = $user->activePosition?->position; + $departmentName = $position?->department?->name ?? '无部门'; + $positionIcon = $position?->icon ?? ''; + $positionName = $position?->name ?? '无职务'; + $vipIcon = $user->vipIcon(); + $vipName = $user->vipName() ?: '普通会员'; + $vipLabel = trim($vipIcon.' '.$vipName); + $positionLabel = trim($positionIcon.' '.$positionName); + + return [ + 'department_name' => $departmentName, + 'position_icon' => $positionIcon, + 'position_name' => $positionName, + 'vip_icon' => $vipIcon, + 'vip_name' => $vipName, + 'vip_label' => $vipLabel, + 'inline' => "部门 {$departmentName} · 职务 {$positionLabel} · 会员 {$vipLabel}", + ]; + } + /** * 读取用户当前仍然有效的当日状态。 * diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index 5a8372e..afaac9f 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -3716,12 +3716,13 @@ now.getSeconds().toString().padStart(2, '0'); const d = data.data; const levelTitle = d.title || '普通会员'; + const identitySummary = d.identity_summary ? `${d.identity_summary} · ` : ''; let levelInfo = ''; if (d.is_max_level) { - levelInfo = `级别(${d.user_level});经验(${d.exp_num});金币(${d.jjb}枚);已满级。`; + levelInfo = `⏰ 自动存点 · ${identitySummary}LV.${d.user_level} · 经验 ${d.exp_num} · 金币 ${d.jjb} · 已满级 ✓`; } else { - levelInfo = `级别(${d.user_level});经验(${d.exp_num});金币(${d.jjb}枚)。`; + levelInfo = `⏰ 自动存点 · ${identitySummary}LV.${d.user_level} · 经验 ${d.exp_num} · 金币 ${d.jjb}`; } // 本次获得的奖励提示 @@ -3730,7 +3731,7 @@ const parts = []; if (d.exp_gain > 0) parts.push(`经验+${d.exp_gain}`); if (d.jjb_gain > 0) parts.push(`金币+${d.jjb_gain}`); - gainInfo = `(本次: ${parts.join(', ')})`; + gainInfo = ` 本次获得:${parts.join(',')}`; } if (data.data.leveled_up) { @@ -3746,7 +3747,7 @@ const detailDiv = document.createElement('div'); detailDiv.className = 'msg-line'; detailDiv.innerHTML = - `【${levelTitle}存点】您的最新情况:${levelInfo} ${gainInfo}(${timeStr})`; + `【${escapeHtml(levelTitle)}存点】${escapeHtml(levelInfo + gainInfo)}(${timeStr})`; container2.appendChild(detailDiv); if (autoScroll) container2.scrollTop = container2.scrollHeight; } else { diff --git a/tests/Feature/AutoSaveExpCommandTest.php b/tests/Feature/AutoSaveExpCommandTest.php new file mode 100644 index 0000000..5936044 --- /dev/null +++ b/tests/Feature/AutoSaveExpCommandTest.php @@ -0,0 +1,101 @@ + 'exp_per_heartbeat'], ['body' => '0']); + Sysparam::updateOrCreate(['alias' => 'jjb_per_heartbeat'], ['body' => '0']); + Sysparam::updateOrCreate(['alias' => 'superlevel'], ['body' => '100']); + Cache::flush(); + + $room = Room::create(['room_name' => 'asave']); + $vipLevel = VipLevel::factory()->create([ + 'name' => '至尊会员', + 'icon' => '👑', + ]); + $user = User::factory()->create([ + 'username' => 'autosave-user', + 'user_level' => 100, + 'exp_num' => 168762, + 'jjb' => 1100017, + 'vip_level_id' => $vipLevel->id, + 'hy_time' => now()->addDay(), + ]); + $department = Department::create([ + 'name' => '办公厅', + 'rank' => 100, + 'color' => '#1d4ed8', + 'sort_order' => 1, + ]); + $position = Position::create([ + 'department_id' => $department->id, + 'name' => '厅长', + 'icon' => '🏛️', + 'rank' => 100, + 'level' => 100, + 'sort_order' => 1, + ]); + UserPosition::create([ + 'user_id' => $user->id, + 'position_id' => $position->id, + 'appointed_at' => now(), + 'is_active' => true, + ]); + + Redis::hset("room:{$room->id}:users", $user->username, json_encode(['username' => $user->username], JSON_UNESCAPED_UNICODE)); + + $this->artisan('chatroom:auto-save-exp')->assertSuccessful(); + + $messages = collect(Redis::lrange("room:{$room->id}:messages", 0, -1)) + ->map(fn (string $message): array => json_decode($message, true)); + $notice = $messages->first(fn (array $message): bool => ($message['to_user'] ?? '') === $user->username); + + $this->assertIsArray($notice); + $this->assertStringContainsString('部门 办公厅 · 职务 🏛️ 厅长 · 会员 👑 至尊会员', $notice['content']); + $this->assertStringContainsString('LV.100 · 经验 168762 · 金币 1100017 · 已满级 ✓', $notice['content']); + } +} diff --git a/tests/Feature/ChatControllerTest.php b/tests/Feature/ChatControllerTest.php index 39d9117..4d8e9ba 100644 --- a/tests/Feature/ChatControllerTest.php +++ b/tests/Feature/ChatControllerTest.php @@ -12,8 +12,10 @@ use App\Events\MessageSent; use App\Models\Department; use App\Models\Position; use App\Models\Room; +use App\Models\Sysparam; use App\Models\User; use App\Models\UserPosition; +use App\Models\VipLevel; use App\Support\PositionPermissionRegistry; use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; @@ -21,6 +23,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Request; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Broadcast; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\URL; @@ -149,6 +152,63 @@ class ChatControllerTest extends TestCase $this->assertSame($position?->department?->name, $authorizedPayload['department_name'] ?? null); } + /** + * 测试手动存点接口会返回部门、职务与会员展示字段,供前端复用自动存点格式。 + */ + public function test_heartbeat_returns_identity_summary_for_save_notice(): void + { + Sysparam::updateOrCreate(['alias' => 'exp_per_heartbeat'], ['body' => '0']); + Sysparam::updateOrCreate(['alias' => 'jjb_per_heartbeat'], ['body' => '0']); + Sysparam::updateOrCreate(['alias' => 'auto_event_chance'], ['body' => '0']); + Sysparam::updateOrCreate(['alias' => 'superlevel'], ['body' => '100']); + Cache::flush(); + + $room = Room::create([ + 'room_name' => 'hbident', + 'door_open' => true, + ]); + $vipLevel = VipLevel::factory()->create([ + 'name' => '至尊会员', + 'icon' => '👑', + ]); + $user = User::factory()->create([ + 'user_level' => 100, + 'exp_num' => 168762, + 'jjb' => 1100017, + 'vip_level_id' => $vipLevel->id, + 'hy_time' => now()->addDay(), + ]); + $department = Department::create([ + 'name' => '办公厅', + 'rank' => 100, + 'color' => '#1d4ed8', + 'sort_order' => 1, + ]); + $position = Position::create([ + 'department_id' => $department->id, + 'name' => '厅长', + 'icon' => '🏛️', + 'rank' => 100, + 'level' => 100, + 'sort_order' => 1, + ]); + UserPosition::create([ + 'user_id' => $user->id, + 'position_id' => $position->id, + 'appointed_at' => now(), + 'is_active' => true, + ]); + + $response = $this->actingAs($user)->postJson(route('chat.heartbeat', $room->id)); + + $response->assertOk(); + $response->assertJsonPath('data.identity_summary', '部门 办公厅 · 职务 🏛️ 厅长 · 会员 👑 至尊会员'); + $response->assertJsonPath('data.department_name', '办公厅'); + $response->assertJsonPath('data.position_name', '厅长'); + $response->assertJsonPath('data.vip_name', '至尊会员'); + $response->assertJsonPath('data.vip_icon', '👑'); + } + /** * 测试聊天室 Presence 频道会返回仍在有效期内的当日状态载荷。 */