From b07f4e971aa2d42226ff2b51cc4eafecf76d90e9 Mon Sep 17 00:00:00 2001 From: lkddi Date: Sun, 26 Apr 2026 20:37:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E7=AD=89=E7=BA=A7=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Admin/LevelExpConfigController.php | 77 +++++++ .../Controllers/Admin/SystemController.php | 18 +- .../Requests/UpdateLevelExpConfigRequest.php | 148 +++++++++++++ resources/views/admin/dashboard.blade.php | 12 - resources/views/admin/layouts/app.blade.php | 4 + .../admin/level-exp-configs/index.blade.php | 207 ++++++++++++++++++ resources/views/admin/ops/index.blade.php | 23 ++ resources/views/admin/system/edit.blade.php | 39 +++- routes/web.php | 4 + .../Feature/AdminDashboardControllerTest.php | 2 + .../AdminLevelExpConfigControllerTest.php | 167 ++++++++++++++ .../Feature/AdminOpsControllerTest.php | 45 ++++ .../Feature/AdminSystemControllerTest.php | 21 ++ 13 files changed, 753 insertions(+), 14 deletions(-) create mode 100644 app/Http/Controllers/Admin/LevelExpConfigController.php create mode 100644 app/Http/Requests/UpdateLevelExpConfigRequest.php create mode 100644 resources/views/admin/level-exp-configs/index.blade.php create mode 100644 tests/Feature/Feature/AdminLevelExpConfigControllerTest.php create mode 100644 tests/Feature/Feature/AdminOpsControllerTest.php diff --git a/app/Http/Controllers/Admin/LevelExpConfigController.php b/app/Http/Controllers/Admin/LevelExpConfigController.php new file mode 100644 index 0000000..d1ca2b5 --- /dev/null +++ b/app/Http/Controllers/Admin/LevelExpConfigController.php @@ -0,0 +1,77 @@ +values() + ->map(fn (int $exp, int $index): array => [ + 'level' => $index + 1, + 'exp' => $exp, + 'increment' => $index === 0 ? $exp : $exp - $rawThresholds[$index - 1], + ]); + + return view('admin.level-exp-configs.index', [ + 'thresholds' => $thresholds, + 'maxLevel' => $maxLevel, + ]); + } + + /** + * 方法功能:保存等级经验阈值配置,并同步刷新缓存。 + */ + public function update(UpdateLevelExpConfigRequest $request): RedirectResponse + { + $thresholds = $request->validated('thresholds'); + + // 将列表页提交的阈值重新拼成兼容旧逻辑的逗号字符串。 + $body = implode(',', $thresholds); + + Sysparam::updateOrCreate( + ['alias' => 'levelexp'], + [ + 'body' => $body, + 'guidetxt' => '按列表逐级维护升级所需的累计经验阈值', + ] + ); + + // 同步更新 Redis / Cache,确保前台经验等级计算即时生效。 + $this->chatState->setSysParam('levelexp', $body); + Sysparam::clearCache('levelexp'); + + return redirect()->route('admin.level-exp-configs.index')->with('success', '等级经验阈值已保存并生效!'); + } +} diff --git a/app/Http/Controllers/Admin/SystemController.php b/app/Http/Controllers/Admin/SystemController.php index e30d1b6..b891ffd 100644 --- a/app/Http/Controllers/Admin/SystemController.php +++ b/app/Http/Controllers/Admin/SystemController.php @@ -60,6 +60,14 @@ class SystemController extends Controller // 只接受通用系统页白名单内的字段,忽略任何伪造提交的敏感键。 $data = $request->only($this->editableSystemAliases()); + if (array_key_exists('maxlevel', $data)) { + $normalizedMaxLevel = max(1, (int) $data['maxlevel']); + + // 管理员级别始终跟随最高等级 + 1,避免两个配置页出现口径漂移。 + $data['maxlevel'] = (string) $normalizedMaxLevel; + $data['superlevel'] = (string) ($normalizedMaxLevel + 1); + } + foreach ($data as $alias => $body) { $normalizedBody = (string) $body; @@ -88,7 +96,7 @@ class SystemController extends Controller return SysParam::query() ->orderBy('id') ->pluck('alias') - ->filter(fn (string $alias): bool => ! $this->isSensitiveAlias($alias)) + ->filter(fn (string $alias): bool => ! $this->isSensitiveAlias($alias) && ! $this->isDedicatedAlias($alias)) ->values() ->all(); } @@ -104,4 +112,12 @@ class SystemController extends Controller return Str::endsWith($alias, ['_password', '_secret', '_token', '_key']); } + + /** + * 判断参数是否已经迁移到独立配置页。 + */ + private function isDedicatedAlias(string $alias): bool + { + return in_array($alias, ['levelexp'], true); + } } diff --git a/app/Http/Requests/UpdateLevelExpConfigRequest.php b/app/Http/Requests/UpdateLevelExpConfigRequest.php new file mode 100644 index 0000000..8f5c533 --- /dev/null +++ b/app/Http/Requests/UpdateLevelExpConfigRequest.php @@ -0,0 +1,148 @@ +input('thresholds', [])) + ->map(fn ($value): string => trim((string) $value)) + ->filter(fn (string $value): bool => $value !== '') + ->values() + ->all(); + + $this->merge([ + 'thresholds' => $thresholds, + ]); + } + + /** + * 方法功能:返回等级经验阈值表单的校验规则。 + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'thresholds' => ['required', 'array', 'min:1', $this->strictlyIncreasingRule(), $this->maxLevelLimitRule()], + 'thresholds.*' => ['required', 'integer', 'min:1'], + ]; + } + + /** + * 方法功能:返回中文校验错误消息。 + * + * @return array + */ + public function messages(): array + { + return [ + 'thresholds.required' => '请至少配置一个等级经验阈值。', + 'thresholds.array' => '等级经验阈值提交格式不正确。', + 'thresholds.min' => '请至少保留一个等级经验阈值。', + 'thresholds.*.required' => '等级经验阈值不能为空。', + 'thresholds.*.integer' => '等级经验阈值必须是整数。', + 'thresholds.*.min' => '等级经验阈值必须大于 0。', + ]; + } + + /** + * 方法功能:自定义校验阈值必须严格递增。 + */ + private function strictlyIncreasingRule(): ValidationRule + { + return new class implements ValidationRule + { + /** + * 方法功能:执行严格递增校验。 + * + * @param Closure(string): void $fail + */ + public function validate(string $attribute, mixed $value, Closure $fail): void + { + if (! is_array($value)) { + return; + } + + $previous = null; + + foreach ($value as $index => $threshold) { + if (! is_numeric($threshold)) { + continue; + } + + $current = (int) $threshold; + + // 每一级累计经验必须大于前一级,避免等级计算出现倒挂。 + if ($previous !== null && $current <= $previous) { + $fail('等级经验阈值必须按等级从小到大严格递增,第 '.($index + 1).' 级配置不正确。'); + + return; + } + + $previous = $current; + } + } + }; + } + + /** + * 方法功能:校验等级阈值数量不能超过用户最高可达等级。 + */ + private function maxLevelLimitRule(): ValidationRule + { + return new class((int) Sysparam::getValue('maxlevel', '99')) implements ValidationRule + { + /** + * 方法功能:构造数量上限校验器。 + */ + public function __construct( + private readonly int $maxLevel + ) {} + + /** + * 方法功能:执行阈值数量与最高等级的上限校验。 + * + * @param Closure(string): void $fail + */ + public function validate(string $attribute, mixed $value, Closure $fail): void + { + if (! is_array($value) || $this->maxLevel < 1) { + return; + } + + // 阈值行数对应可升级的等级数,不能超过用户最高可达等级。 + if (count($value) > $this->maxLevel) { + $fail('等级经验阈值数量不能超过用户最高可达等级,请先提高最高等级或删除多余等级。'); + } + } + }; + } +} diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index e516af0..75a6eea 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -35,18 +35,6 @@ PHP 版本: {{ PHP_VERSION }} -
  • - 队列监控面板 - - - 打开 Horizon 控制台 - - - - -
  • diff --git a/resources/views/admin/layouts/app.blade.php b/resources/views/admin/layouts/app.blade.php index a31be6b..b80848d 100644 --- a/resources/views/admin/layouts/app.blade.php +++ b/resources/views/admin/layouts/app.blade.php @@ -64,6 +64,10 @@ class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.system.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}"> {!! '⚙️ 聊天室参数' !!} + + {!! '📶 等级经验阈值' !!} + {!! '💴 用户流水' !!} diff --git a/resources/views/admin/level-exp-configs/index.blade.php b/resources/views/admin/level-exp-configs/index.blade.php new file mode 100644 index 0000000..c648fca --- /dev/null +++ b/resources/views/admin/level-exp-configs/index.blade.php @@ -0,0 +1,207 @@ +@extends('admin.layouts.app') + +@section('title', '等级经验阈值管理') + +@section('content') + @php require resource_path('views/admin/partials/list-theme.php'); @endphp + @php + $formThresholds = collect(old('thresholds', $thresholds->pluck('exp')->all())) + ->map(fn ($value) => trim((string) $value)) + ->filter(fn (string $value) => $value !== '') + ->values(); + + if ($formThresholds->isEmpty()) { + $formThresholds = $thresholds->pluck('exp')->map(fn ($value) => (string) $value)->values(); + } + @endphp + +
    +
    +

    按列表维护每一级升级所需的累计经验值,并统一管理用户最高可达等级与管理员级别。

    +
    + +
    + @csrf + @method('PUT') + +
    +
    +
    等级阈值说明
    +

    + 当前页面按列表逐级维护升级经验阈值:每一行对应一个等级,填写“升到该等级所需的累计经验值”。 + 等级阈值必须严格递增,且等级行数不能超过“用户最高可达等级”。 +

    +
    +
    + +
    + + + + + + + + + + + @foreach ($formThresholds as $index => $exp) + @php + $currentExp = (int) $exp; + $previousExp = $index === 0 ? 0 : (int) $formThresholds[$index - 1]; + @endphp + + + + + + + @endforeach + +
    等级累计经验阈值较上一等级新增操作
    第 {{ $index + 1 }} 级 + + + +{{ number_format($currentExp - $previousExp) }} + + +
    +
    + +
    +
    + +

    + 当前已配置 {{ $formThresholds->count() }} 个等级阈值,最高可配置到 {{ $maxLevel }} 级。 +

    +
    + + +
    +
    +
    + + + + +@endsection diff --git a/resources/views/admin/ops/index.blade.php b/resources/views/admin/ops/index.blade.php index ca475d2..c382ddf 100644 --- a/resources/views/admin/ops/index.blade.php +++ b/resources/views/admin/ops/index.blade.php @@ -81,6 +81,29 @@ + {{-- 队列监控面板 --}} +
    + {{-- 房间在线名单清理 --}}
    diff --git a/resources/views/admin/system/edit.blade.php b/resources/views/admin/system/edit.blade.php index 8a7e63c..44852ba 100644 --- a/resources/views/admin/system/edit.blade.php +++ b/resources/views/admin/system/edit.blade.php @@ -25,13 +25,24 @@ @php $fieldValue = (string) $body; $shouldUseTextarea = strlen($fieldValue) > 50 || str_contains($fieldValue, "\n") || str_contains($fieldValue, '<'); + $isMaxLevelField = $alias === 'maxlevel'; + $isSuperLevelField = $alias === 'superlevel'; @endphp
    - @if ($shouldUseTextarea) + @if ($isMaxLevelField) +

    修改后会自动同步管理员级别为“最高等级 + 1”。

    + + @elseif ($isSuperLevelField) +

    该值会随“用户最高可达等级”自动计算,仅用于展示当前结果。

    + + @elseif ($shouldUseTextarea) @else @@ -53,4 +64,30 @@
    + + @endsection diff --git a/routes/web.php b/routes/web.php index e67eee7..f2dc801 100644 --- a/routes/web.php +++ b/routes/web.php @@ -445,6 +445,10 @@ Route::middleware(['chat.auth', 'chat.has_position'])->prefix('admin')->name('ad Route::get('/system', [\App\Http\Controllers\Admin\SystemController::class, 'edit'])->name('system.edit'); Route::put('/system', [\App\Http\Controllers\Admin\SystemController::class, 'update'])->name('system.update'); + // 等级经验阈值配置 + Route::get('/level-exp-configs', [\App\Http\Controllers\Admin\LevelExpConfigController::class, 'index'])->name('level-exp-configs.index'); + Route::put('/level-exp-configs', [\App\Http\Controllers\Admin\LevelExpConfigController::class, 'update'])->name('level-exp-configs.update'); + // 微信机器人配置 Route::get('/wechat-bot', [\App\Http\Controllers\Admin\WechatBotController::class, 'edit'])->name('wechat_bot.edit'); Route::put('/wechat-bot', [\App\Http\Controllers\Admin\WechatBotController::class, 'update'])->name('wechat_bot.update'); diff --git a/tests/Feature/Feature/AdminDashboardControllerTest.php b/tests/Feature/Feature/AdminDashboardControllerTest.php index ddb3f1e..09040a0 100644 --- a/tests/Feature/Feature/AdminDashboardControllerTest.php +++ b/tests/Feature/Feature/AdminDashboardControllerTest.php @@ -61,6 +61,8 @@ class AdminDashboardControllerTest extends TestCase $response->assertOk(); $response->assertSee('当前在线人数'); $response->assertSee('2'); + $response->assertDontSee('队列监控面板'); + $response->assertDontSee('打开 Horizon 控制台'); } /** diff --git a/tests/Feature/Feature/AdminLevelExpConfigControllerTest.php b/tests/Feature/Feature/AdminLevelExpConfigControllerTest.php new file mode 100644 index 0000000..01dfe13 --- /dev/null +++ b/tests/Feature/Feature/AdminLevelExpConfigControllerTest.php @@ -0,0 +1,167 @@ +seedLevelExpParam(); + $admin = $this->createSuperAdmin(); + + $response = $this->actingAs($admin)->get(route('admin.level-exp-configs.index')); + + $response->assertOk(); + $response->assertSee('等级经验阈值管理'); + $response->assertSee('第 1 级'); + $response->assertSee('第 3 级'); + $response->assertSee('10'); + $response->assertSee('150'); + $response->assertSee('最高可配置到 99 级'); + } + + /** + * 方法功能:验证独立配置页可保存新的等级经验阈值。 + */ + public function test_level_exp_update_persists_thresholds(): void + { + $this->seedLevelExpParam(); + $admin = $this->createSuperAdmin(); + + $response = $this->actingAs($admin)->put(route('admin.level-exp-configs.update'), [ + 'thresholds' => ['20', '80', '180', '360'], + ]); + + $response->assertRedirect(route('admin.level-exp-configs.index')); + $response->assertSessionHas('success'); + + $this->assertDatabaseHas('sysparam', [ + 'alias' => 'levelexp', + 'body' => '20,80,180,360', + ]); + $this->assertDatabaseHas('sysparam', [ + 'alias' => 'maxlevel', + 'body' => '99', + ]); + $this->assertDatabaseHas('sysparam', [ + 'alias' => 'superlevel', + 'body' => '100', + ]); + } + + /** + * 方法功能:验证阈值必须严格递增,防止错误配置写入。 + */ + public function test_level_exp_update_requires_strictly_increasing_thresholds(): void + { + $this->seedLevelExpParam(); + $admin = $this->createSuperAdmin(); + + $response = $this->from(route('admin.level-exp-configs.index')) + ->actingAs($admin) + ->put(route('admin.level-exp-configs.update'), [ + 'thresholds' => ['20', '18', '100'], + ]); + + $response->assertRedirect(route('admin.level-exp-configs.index')); + $response->assertSessionHasErrors('thresholds'); + + $this->assertDatabaseHas('sysparam', [ + 'alias' => 'levelexp', + 'body' => '10,50,150', + ]); + } + + /** + * 方法功能:验证等级数量不能超过用户最高可达等级。 + */ + public function test_level_exp_update_requires_threshold_count_not_exceed_maxlevel(): void + { + $this->seedLevelExpParam(); + Sysparam::updateOrCreate( + ['alias' => 'maxlevel'], + ['body' => '2', 'guidetxt' => '用户最高可达等级'] + ); + $admin = $this->createSuperAdmin(); + + $response = $this->from(route('admin.level-exp-configs.index')) + ->actingAs($admin) + ->put(route('admin.level-exp-configs.update'), [ + 'thresholds' => ['20', '80', '180'], + ]); + + $response->assertRedirect(route('admin.level-exp-configs.index')); + $response->assertSessionHasErrors('thresholds'); + + $this->assertDatabaseHas('sysparam', [ + 'alias' => 'levelexp', + 'body' => '10,50,150', + ]); + $this->assertDatabaseHas('sysparam', [ + 'alias' => 'maxlevel', + 'body' => '2', + ]); + $this->assertDatabaseHas('sysparam', [ + 'alias' => 'superlevel', + 'body' => '100', + ]); + } + + /** + * 方法功能:创建可访问后台页面的超级管理员。 + */ + private function createSuperAdmin(): User + { + return User::factory()->create([ + 'user_level' => 100, + ]); + } + + /** + * 方法功能:初始化等级经验阈值参数。 + */ + private function seedLevelExpParam(): void + { + $rows = [ + 'levelexp' => [ + 'body' => '10,50,150', + 'guidetxt' => '按列表逐级维护升级所需的累计经验阈值', + ], + 'maxlevel' => [ + 'body' => '99', + 'guidetxt' => '用户最高可达等级', + ], + 'superlevel' => [ + 'body' => '100', + 'guidetxt' => '管理员级别(= 最高等级 + 1,拥有最高权限的等级阈值)', + ], + ]; + + foreach ($rows as $alias => $payload) { + Sysparam::updateOrCreate( + ['alias' => $alias], + $payload + ); + } + } +} diff --git a/tests/Feature/Feature/AdminOpsControllerTest.php b/tests/Feature/Feature/AdminOpsControllerTest.php new file mode 100644 index 0000000..deda6bc --- /dev/null +++ b/tests/Feature/Feature/AdminOpsControllerTest.php @@ -0,0 +1,45 @@ +create([ + 'id' => 1, + 'username' => 'site-owner', + 'user_level' => 100, + ]); + + $response = $this->actingAs($siteOwner)->get(route('admin.ops.index')); + + $response->assertOk(); + $response->assertSee('队列监控面板'); + $response->assertSee('打开 Horizon 控制台'); + $response->assertSee('/horizon'); + } +} diff --git a/tests/Feature/Feature/AdminSystemControllerTest.php b/tests/Feature/Feature/AdminSystemControllerTest.php index 7d7b970..0e2dd4b 100644 --- a/tests/Feature/Feature/AdminSystemControllerTest.php +++ b/tests/Feature/Feature/AdminSystemControllerTest.php @@ -38,6 +38,9 @@ class AdminSystemControllerTest extends TestCase $response->assertDontSee('vip_payment_app_secret'); $response->assertDontSee('wechat_bot_config'); $response->assertDontSee('chatbot_max_gold'); + $response->assertDontSee('levelexp'); + $response->assertSee('maxlevel'); + $response->assertSee('superlevel'); } /** @@ -51,6 +54,9 @@ class AdminSystemControllerTest extends TestCase $response = $this->actingAs($admin)->put(route('admin.system.update'), [ 'sys_name' => '新版聊天室', 'sys_notice' => '新的公共公告', + 'levelexp' => '20,80,180', + 'maxlevel' => '88', + 'superlevel' => '666', 'smtp_host' => 'attacker.smtp.example', 'vip_payment_app_secret' => 'tampered-secret', 'wechat_bot_config' => '{"api":{"bot_key":"stolen"}}', @@ -69,6 +75,18 @@ class AdminSystemControllerTest extends TestCase 'alias' => 'sys_notice', 'body' => '新的公共公告', ]); + $this->assertDatabaseHas('sysparam', [ + 'alias' => 'levelexp', + 'body' => '10,50,150', + ]); + $this->assertDatabaseHas('sysparam', [ + 'alias' => 'maxlevel', + 'body' => '88', + ]); + $this->assertDatabaseHas('sysparam', [ + 'alias' => 'superlevel', + 'body' => '89', + ]); // 敏感配置必须保持原值,不能被通用系统页伪造请求覆盖。 $this->assertDatabaseHas('sysparam', [ @@ -128,6 +146,9 @@ class AdminSystemControllerTest extends TestCase return [ 'sys_name' => '原始聊天室', 'sys_notice' => '原始公告', + 'levelexp' => '10,50,150', + 'maxlevel' => '99', + 'superlevel' => '100', 'smtp_host' => 'owner.smtp.example', 'vip_payment_app_secret' => 'owner-secret', 'wechat_bot_config' => '{"api":{"bot_key":"owner-only"}}',