优化屏蔽,可以保存状态
This commit is contained in:
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:聊天室偏好设置验证器
|
||||
* 负责校验用户提交的屏蔽播报与禁音配置。
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* 聊天室偏好设置验证器
|
||||
* 仅允许提交白名单内的屏蔽项与布尔型禁音状态。
|
||||
*/
|
||||
class UpdateChatPreferencesRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* 允许已登录用户保存自己的聊天室偏好。
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天室偏好的验证规则。
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'blocked_system_senders' => ['nullable', 'array'],
|
||||
'blocked_system_senders.*' => [
|
||||
'string',
|
||||
Rule::in(['钓鱼播报', '星海小博士', '百家乐', '跑马']),
|
||||
],
|
||||
'sound_muted' => ['required', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天室偏好的中文错误提示。
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'blocked_system_senders.array' => '屏蔽设置格式无效。',
|
||||
'blocked_system_senders.*.in' => '存在不支持的屏蔽项目。',
|
||||
'sound_muted.required' => '请传入禁音状态。',
|
||||
'sound_muted.boolean' => '禁音状态格式无效。',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 为用户表增加聊天室偏好配置字段。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->json('chat_preferences')->nullable()->after('custom_leave_effect')->comment('聊天室屏蔽与禁音偏好配置');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚用户表中的聊天室偏好配置字段。
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('chat_preferences');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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 }},
|
||||
|
||||
@@ -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<string, any>} 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;
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户控制器功能测试
|
||||
* 覆盖个人资料、密码与聊天室偏好设置等关键接口。
|
||||
*/
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Room;
|
||||
@@ -15,6 +20,9 @@ class UserControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* 初始化用户控制器测试所需的系统参数。
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -27,6 +35,9 @@ class UserControllerTest extends TestCase
|
||||
Sysparam::updateOrCreate(['alias' => '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([
|
||||
|
||||
Reference in New Issue
Block a user