From 1a39ddd7259d65358a5dcc5832cff8bc6e6e08c6 Mon Sep 17 00:00:00 2001 From: lkddi Date: Tue, 14 Apr 2026 22:48:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=B1=8F=E8=94=BD=EF=BC=8C?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E4=BF=9D=E5=AD=98=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/UserController.php | 27 +++- .../Requests/UpdateChatPreferencesRequest.php | 58 +++++++++ app/Models/User.php | 2 + ...17_add_chat_preferences_to_users_table.php | 28 +++++ resources/views/chat/frame.blade.php | 2 + .../views/chat/partials/scripts.blade.php | 119 +++++++++++++++++- routes/web.php | 1 + tests/Feature/UserControllerTest.php | 47 +++++++ 8 files changed, 279 insertions(+), 5 deletions(-) create mode 100644 app/Http/Requests/UpdateChatPreferencesRequest.php create mode 100644 database/migrations/2026_04_14_224517_add_chat_preferences_to_users_table.php diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 73a1328..365aabd 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -20,6 +20,7 @@ namespace App\Http\Controllers; use App\Events\UserKicked; use App\Events\UserMuted; use App\Http\Requests\ChangePasswordRequest; +use App\Http\Requests\UpdateChatPreferencesRequest; use App\Http\Requests\UpdateProfileRequest; use App\Models\Room; use App\Models\Sysparam; @@ -109,7 +110,6 @@ class UserController extends Controller $data['vip']['Name'] = $targetUser->vipName(); $data['vip']['Icon'] = $targetUser->vipIcon(); - // 拥有封禁IP(level_banip)或踢人以上权限的管理,可以查看IP和归属地 $levelBanIp = (int) Sysparam::getValue('level_banip', '15'); if ($operator && $operator->user_level >= $levelBanIp) { @@ -203,6 +203,31 @@ class UserController extends Controller return response()->json(['status' => 'success', 'message' => '资料更新成功。']); } + /** + * 保存聊天室屏蔽与禁音偏好。 + */ + public function updateChatPreferences(UpdateChatPreferencesRequest $request): JsonResponse + { + $user = Auth::user(); + $data = $request->validated(); + + $preferences = [ + // 去重并重建索引,保持存储结构稳定,便于后续继续扩展其它屏蔽项。 + 'blocked_system_senders' => array_values(array_unique($data['blocked_system_senders'] ?? [])), + 'sound_muted' => (bool) $data['sound_muted'], + ]; + + $user->update([ + 'chat_preferences' => $preferences, + ]); + + return response()->json([ + 'status' => 'success', + 'message' => '聊天室偏好已保存。', + 'data' => $preferences, + ]); + } + /** * 修改密码 (对应 chpasswd.asp) */ diff --git a/app/Http/Requests/UpdateChatPreferencesRequest.php b/app/Http/Requests/UpdateChatPreferencesRequest.php new file mode 100644 index 0000000..4b4e54c --- /dev/null +++ b/app/Http/Requests/UpdateChatPreferencesRequest.php @@ -0,0 +1,58 @@ +user() !== null; + } + + /** + * 获取聊天室偏好的验证规则。 + * + * @return array + */ + public function rules(): array + { + return [ + 'blocked_system_senders' => ['nullable', 'array'], + 'blocked_system_senders.*' => [ + 'string', + Rule::in(['钓鱼播报', '星海小博士', '百家乐', '跑马']), + ], + 'sound_muted' => ['required', 'boolean'], + ]; + } + + /** + * 获取聊天室偏好的中文错误提示。 + * + * @return array + */ + public function messages(): array + { + return [ + 'blocked_system_senders.array' => '屏蔽设置格式无效。', + 'blocked_system_senders.*.in' => '存在不支持的屏蔽项目。', + 'sound_muted.required' => '请传入禁音状态。', + 'sound_muted.boolean' => '禁音状态格式无效。', + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 156040f..36aa2bd 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -49,6 +49,7 @@ class User extends Authenticatable 'custom_leave_message', 'custom_join_effect', 'custom_leave_effect', + 'chat_preferences', 'user_level', 'inviter_id', 'room_id', @@ -101,6 +102,7 @@ class User extends Authenticatable 'sj' => 'datetime', 'q3_time' => 'datetime', 'has_received_new_gift' => 'boolean', + 'chat_preferences' => 'array', ]; } diff --git a/database/migrations/2026_04_14_224517_add_chat_preferences_to_users_table.php b/database/migrations/2026_04_14_224517_add_chat_preferences_to_users_table.php new file mode 100644 index 0000000..a38f0a7 --- /dev/null +++ b/database/migrations/2026_04_14_224517_add_chat_preferences_to_users_table.php @@ -0,0 +1,28 @@ +json('chat_preferences')->nullable()->after('custom_leave_effect')->comment('聊天室屏蔽与禁音偏好配置'); + }); + } + + /** + * 回滚用户表中的聊天室偏好配置字段。 + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('chat_preferences'); + }); + } +}; diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php index 5f35eb0..e185e53 100644 --- a/resources/views/chat/frame.blade.php +++ b/resources/views/chat/frame.blade.php @@ -94,8 +94,10 @@ revokeUrl: "{{ route('chat.appoint.revoke') }}", rewardUrl: "{{ route('command.reward') }}", rewardQuotaUrl: "{{ route('command.reward_quota') }}", + chatPreferencesUrl: "{{ route('user.update_chat_preferences') }}", userJjb: {{ (int) $user->jjb }}, // 当前用户金币(求婚前金额预检查用) myGold: {{ (int) $user->jjb }}, // 赠金币面板显示余额用(赠送成功后前端更新) + chatPreferences: @json($user->chat_preferences ?? []), // ─── 婚姻系统 ────────────────────────────── minWeddingCost: {{ (int) \App\Models\WeddingTier::where('is_active', true)->orderBy('amount')->value('amount') ?? 0 }}, diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index 3f6bebf..00a2135 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -38,6 +38,7 @@ const onlineCount = document.getElementById('online-count'); const onlineCountBottom = document.getElementById('online-count-bottom'); const BLOCKED_SYSTEM_SENDERS_STORAGE_KEY = 'chat_blocked_system_senders'; + const CHAT_SOUND_MUTED_STORAGE_KEY = 'chat_sound_muted'; const BLOCKABLE_SYSTEM_SENDERS = ['钓鱼播报', '星海小博士', '百家乐', '跑马']; // ── 消息区:手机端双触发打开用户名片(PC 端靠 ondblclick 内联属性)── @@ -70,7 +71,26 @@ let onlineUsers = {}; let autoScroll = true; let _maxMsgId = 0; // 记录当前收到的最大消息 ID - let blockedSystemSenders = new Set(loadBlockedSystemSenders()); + const initialChatPreferences = normalizeChatPreferences(window.chatContext?.chatPreferences || {}); + let blockedSystemSenders = new Set(initialChatPreferences.blocked_system_senders); + + /** + * 规整聊天室偏好对象,过滤非法配置并补齐默认值。 + * + * @param {Record} raw 原始偏好对象 + * @returns {Object} + */ + function normalizeChatPreferences(raw) { + const blocked = Array.isArray(raw?.blocked_system_senders) + ? raw.blocked_system_senders.filter(sender => BLOCKABLE_SYSTEM_SENDERS.includes(sender)) + : []; + + return { + // 默认所有用户都处于“不屏蔽”的开放状态,只有显式勾选的项目才会进入该列表。 + blocked_system_senders: Array.from(new Set(blocked)), + sound_muted: Boolean(raw?.sound_muted), + }; + } /** * 从 localStorage 读取已屏蔽的系统播报发送者列表。 @@ -101,6 +121,77 @@ ); } + /** + * 判断当前禁音开关是否处于打开状态。 + * + * @returns {boolean} + */ + function isSoundMuted() { + const muteCheckbox = document.getElementById('sound_muted'); + + if (muteCheckbox) { + return Boolean(muteCheckbox.checked); + } + + return localStorage.getItem(CHAT_SOUND_MUTED_STORAGE_KEY) === '1'; + } + + /** + * 获取当前聊天室偏好快照。 + * + * @returns {Object} + */ + function buildChatPreferencesPayload() { + return { + blocked_system_senders: Array.from(blockedSystemSenders), + sound_muted: isSoundMuted(), + }; + } + + /** + * 将聊天室偏好写入本地缓存,供刷新前快速恢复与迁移兜底。 + */ + function persistChatPreferencesToLocal() { + persistBlockedSystemSenders(); + localStorage.setItem(CHAT_SOUND_MUTED_STORAGE_KEY, isSoundMuted() ? '1' : '0'); + } + + /** + * 将当前聊天室偏好保存到当前登录账号。 + */ + async function saveChatPreferences() { + const payload = buildChatPreferencesPayload(); + + persistChatPreferencesToLocal(); + + if (!window.chatContext?.chatPreferencesUrl) { + return; + } + + try { + const response = await fetch(window.chatContext.chatPreferencesUrl, { + method: 'PUT', + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '', + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error('save chat preferences failed'); + } + + const data = await response.json(); + if (data?.status === 'success') { + window.chatContext.chatPreferences = normalizeChatPreferences(data.data || payload); + } + } catch (error) { + console.error('聊天室偏好保存失败:', error); + } + } + /** * 同步屏蔽菜单中的复选框状态。 */ @@ -237,6 +328,7 @@ persistBlockedSystemSenders(); syncBlockedSystemSenderCheckboxes(); + void saveChatPreferences(); } syncBlockedSystemSenderCheckboxes(); @@ -1589,11 +1681,28 @@ if (saved) { applyFontSize(saved); } - // 恢复禁音复选框状态 - const muted = localStorage.getItem('chat_sound_muted') === '1'; + + const storedBlockedSystemSenders = loadBlockedSystemSenders(); + const mutedFromLocal = localStorage.getItem(CHAT_SOUND_MUTED_STORAGE_KEY) === '1'; + const hasServerPreferences = initialChatPreferences.blocked_system_senders.length > 0 || initialChatPreferences.sound_muted; + const shouldMigrateLocalPreferences = !hasServerPreferences + && (storedBlockedSystemSenders.length > 0 || mutedFromLocal); + + if (shouldMigrateLocalPreferences) { + blockedSystemSenders = new Set(storedBlockedSystemSenders); + } + + // 恢复禁音复选框状态;默认一律为未禁音。 + const muted = shouldMigrateLocalPreferences ? mutedFromLocal : initialChatPreferences.sound_muted; const muteChk = document.getElementById('sound_muted'); if (muteChk) muteChk.checked = muted; syncBlockedSystemSenderCheckboxes(); + + if (shouldMigrateLocalPreferences) { + void saveChatPreferences(); + } else { + persistChatPreferencesToLocal(); + } }); // ── 特效禁音开关 ───────────────────────────────────────────────── @@ -1604,10 +1713,12 @@ * @param {boolean} muted true = 禁音,false = 开启声音 */ function toggleSoundMute(muted) { - localStorage.setItem('chat_sound_muted', muted ? '1' : '0'); + localStorage.setItem(CHAT_SOUND_MUTED_STORAGE_KEY, muted ? '1' : '0'); if (muted && typeof EffectSounds !== 'undefined') { EffectSounds.stop(); // 立即停止当前音效 } + + void saveChatPreferences(); } window.toggleSoundMute = toggleSoundMute; diff --git a/routes/web.php b/routes/web.php index f149299..3d0335c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -85,6 +85,7 @@ Route::middleware(['chat.auth'])->group(function () { // ---- 第七阶段:用户资料与特权管理 ---- Route::get('/user/{username}', [UserController::class, 'show'])->name('user.show'); Route::put('/user/profile', [UserController::class, 'updateProfile'])->name('user.update_profile'); + Route::put('/user/chat-preferences', [UserController::class, 'updateChatPreferences'])->name('user.update_chat_preferences'); Route::post('/user/generate-wechat-code', [UserController::class, 'generateWechatCode'])->name('user.generate_wechat_code'); Route::post('/user/unbind-wechat', [UserController::class, 'unbindWechat'])->name('user.unbind_wechat'); Route::post('/user/send-email-code', [\App\Http\Controllers\Api\VerificationController::class, 'sendEmailCode'])->name('user.send_email_code'); diff --git a/tests/Feature/UserControllerTest.php b/tests/Feature/UserControllerTest.php index 5dc0145..5bfd198 100644 --- a/tests/Feature/UserControllerTest.php +++ b/tests/Feature/UserControllerTest.php @@ -1,5 +1,10 @@ 'smtp_enabled'], ['body' => '1']); // Allow email changing in tests } + /** + * 测试可以查看用户资料卡接口。 + */ public function test_can_view_user_profile() { $user = User::factory()->create([ @@ -43,6 +54,9 @@ class UserControllerTest extends TestCase ->assertJsonPath('data.user_level', 10); } + /** + * 测试不改邮箱时可以正常更新个人资料。 + */ public function test_can_update_profile_without_email_change() { $user = User::factory()->create([ @@ -67,6 +81,9 @@ class UserControllerTest extends TestCase $this->assertEquals('new sign', $user->sign); } + /** + * 测试改邮箱但未提交验证码时会被拒绝。 + */ public function test_cannot_update_email_without_verification_code() { $user = User::factory()->create([ @@ -87,6 +104,9 @@ class UserControllerTest extends TestCase ->assertJsonPath('message', '新邮箱需要验证码,请先获取并填写验证码。'); } + /** + * 测试提供有效验证码后可以成功修改邮箱。 + */ public function test_can_update_email_with_valid_code() { $user = User::factory()->create([ @@ -111,6 +131,33 @@ class UserControllerTest extends TestCase $this->assertEquals('new@example.com', $user->email); } + /** + * 测试可以保存聊天室屏蔽与禁音偏好。 + */ + public function test_can_update_chat_preferences(): void + { + $user = User::factory()->create([ + 'chat_preferences' => null, + ]); + + $response = $this->actingAs($user)->putJson('/user/chat-preferences', [ + 'blocked_system_senders' => ['钓鱼播报', '跑马'], + 'sound_muted' => true, + ]); + + $response->assertOk() + ->assertJsonPath('status', 'success') + ->assertJsonPath('data.blocked_system_senders.0', '钓鱼播报') + ->assertJsonPath('data.blocked_system_senders.1', '跑马') + ->assertJsonPath('data.sound_muted', true); + + $user->refresh(); + $this->assertEquals([ + 'blocked_system_senders' => ['钓鱼播报', '跑马'], + 'sound_muted' => true, + ], $user->chat_preferences); + } + public function test_can_change_password() { $user = User::factory()->create([