From 7fb86bfe214cf06c2150d6fbd0690435a55c48e4 Mon Sep 17 00:00:00 2001 From: lkddi Date: Fri, 27 Feb 2026 15:57:12 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=E5=95=86=E5=BA=97=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E5=AE=9E=E7=8E=B0=EF=BC=88=E5=8D=95=E6=AC=A1?= =?UTF-8?q?=E7=89=B9=E6=95=88=E5=8D=A1888/=E5=91=A8=E5=8D=A18888/=E6=94=B9?= =?UTF-8?q?=E5=90=8D=E5=8D=A15000=EF=BC=8C=E5=90=AB=E8=B4=AD=E4=B9=B0?= =?UTF-8?q?=E3=80=81=E5=91=A8=E5=8D=A1=E8=A6=86=E7=9B=96=E3=80=81=E6=94=B9?= =?UTF-8?q?=E5=90=8D=E9=BB=91=E5=90=8D=E5=8D=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/ChatController.php | 6 +- app/Http/Controllers/ShopController.php | 105 +++++++ app/Models/ShopItem.php | 81 ++++++ app/Models/UserPurchase.php | 57 ++++ app/Models/UsernameBlacklist.php | 34 +++ app/Services/ShopService.php | 222 ++++++++++++++ ...6_02_27_074855_create_shop_items_table.php | 44 +++ ..._27_074855_create_user_purchases_table.php | 49 ++++ ...074856_create_username_blacklist_table.php | 36 +++ database/seeders/ShopItemSeeder.php | 61 ++++ resources/views/chat/frame.blade.php | 11 + .../views/chat/partials/right-panel.blade.php | 9 +- .../views/chat/partials/scripts.blade.php | 10 + .../views/chat/partials/shop-panel.blade.php | 273 ++++++++++++++++++ routes/web.php | 5 + 15 files changed, 999 insertions(+), 4 deletions(-) create mode 100644 app/Http/Controllers/ShopController.php create mode 100644 app/Models/ShopItem.php create mode 100644 app/Models/UserPurchase.php create mode 100644 app/Models/UsernameBlacklist.php create mode 100644 app/Services/ShopService.php create mode 100644 database/migrations/2026_02_27_074855_create_shop_items_table.php create mode 100644 database/migrations/2026_02_27_074855_create_user_purchases_table.php create mode 100644 database/migrations/2026_02_27_074856_create_username_blacklist_table.php create mode 100644 database/seeders/ShopItemSeeder.php create mode 100644 resources/views/chat/partials/shop-panel.blade.php diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php index fe03245..9e08efa 100644 --- a/app/Http/Controllers/ChatController.php +++ b/app/Http/Controllers/ChatController.php @@ -36,6 +36,7 @@ class ChatController extends Controller private readonly ChatStateService $chatState, private readonly MessageFilterService $filter, private readonly VipService $vipService, + private readonly \App\Services\ShopService $shopService, ) {} /** @@ -97,8 +98,9 @@ class ChatController extends Controller // 渲染主聊天框架视图 return view('chat.frame', [ - 'room' => $room, - 'user' => $user, + 'room' => $room, + 'user' => $user, + 'weekEffect' => $this->shopService->getActiveWeekEffect($user), // 周卡特效(登录自动播放) ]); } diff --git a/app/Http/Controllers/ShopController.php b/app/Http/Controllers/ShopController.php new file mode 100644 index 0000000..9c7e38e --- /dev/null +++ b/app/Http/Controllers/ShopController.php @@ -0,0 +1,105 @@ +map(fn ($item) => [ + 'id' => $item->id, + 'name' => $item->name, + 'slug' => $item->slug, + 'description' => $item->description, + 'icon' => $item->icon, + 'price' => $item->price, + 'type' => $item->type, + 'duration_days' => $item->duration_days, + ]); + + return response()->json([ + 'items' => $items, + 'user_jjb' => $user->jjb ?? 0, + 'active_week_effect' => $this->shopService->getActiveWeekEffect($user), + 'has_rename_card' => $this->shopService->hasRenameCard($user), + ]); + } + + /** + * 购买商品 + * + * @param Request $request 含 item_id + */ + public function buy(Request $request): JsonResponse + { + $request->validate(['item_id' => 'required|integer|exists:shop_items,id']); + + $item = ShopItem::find($request->item_id); + if (! $item->is_active) { + return response()->json(['status' => 'error', 'message' => '该商品已下架。'], 400); + } + + $result = $this->shopService->buyItem(Auth::user(), $item); + + if (! $result['ok']) { + return response()->json(['status' => 'error', 'message' => $result['message']], 400); + } + + $response = ['status' => 'success', 'message' => $result['message']]; + + // 单次特效卡:告诉前端立即播放哪个特效 + if (isset($result['play_effect'])) { + $response['play_effect'] = $result['play_effect']; + } + + // 返回最新金币余额 + $response['jjb'] = Auth::user()->fresh()->jjb; + + return response()->json($response); + } + + /** + * 使用改名卡修改昵称 + * + * @param Request $request 含 new_name + */ + public function rename(Request $request): JsonResponse + { + $request->validate([ + 'new_name' => 'required|string|min:1|max:10', + ]); + + $result = $this->shopService->useRenameCard(Auth::user(), $request->new_name); + + if (! $result['ok']) { + return response()->json(['status' => 'error', 'message' => $result['message']], 400); + } + + return response()->json(['status' => 'success', 'message' => $result['message']]); + } +} diff --git a/app/Models/ShopItem.php b/app/Models/ShopItem.php new file mode 100644 index 0000000..c8d7745 --- /dev/null +++ b/app/Models/ShopItem.php @@ -0,0 +1,81 @@ + 'boolean', + ]; + + /** + * 获取该商品的所有购买记录 + */ + public function purchases(): HasMany + { + return $this->hasMany(UserPurchase::class); + } + + /** + * 是否为特效类商品(instant 或 duration,slug 以 once_ 或 week_ 开头) + */ + public function isEffect(): bool + { + return str_starts_with($this->slug, 'once_') || str_starts_with($this->slug, 'week_'); + } + + /** + * 是否为周卡(duration 类型) + */ + public function isWeekCard(): bool + { + return $this->type === 'duration'; + } + + /** + * 是否为单次卡(instant 类型) + */ + public function isInstant(): bool + { + return $this->type === 'instant'; + } + + /** + * 获取特效 key(去掉 once_ / week_ 前缀,返回 fireworks/rain/lightning/snow) + */ + public function effectKey(): ?string + { + if (str_starts_with($this->slug, 'once_')) { + return substr($this->slug, 5); + } + if (str_starts_with($this->slug, 'week_')) { + return substr($this->slug, 5); + } + + return null; + } + + /** + * 获取所有上架商品(按排序) + */ + public static function active(): Collection + { + return static::where('is_active', true)->orderBy('sort_order')->get(); + } +} diff --git a/app/Models/UserPurchase.php b/app/Models/UserPurchase.php new file mode 100644 index 0000000..f46b4d7 --- /dev/null +++ b/app/Models/UserPurchase.php @@ -0,0 +1,57 @@ + 'datetime', + 'used_at' => 'datetime', + ]; + + /** + * 购买记录所属用户 + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * 购买记录对应的商品 + */ + public function shopItem(): BelongsTo + { + return $this->belongsTo(ShopItem::class, 'shop_item_id'); + } + + /** + * 判断周卡是否仍在有效期内 + */ + public function isAlive(): bool + { + if ($this->status !== 'active') { + return false; + } + if ($this->expires_at && $this->expires_at->isPast()) { + return false; + } + + return true; + } +} diff --git a/app/Models/UsernameBlacklist.php b/app/Models/UsernameBlacklist.php new file mode 100644 index 0000000..10f92ca --- /dev/null +++ b/app/Models/UsernameBlacklist.php @@ -0,0 +1,34 @@ + 'datetime', + 'created_at' => 'datetime', + ]; + + /** + * 判断给定名称是否在黑名单有效期内 + */ + public static function isReserved(string $username): bool + { + return static::where('username', $username) + ->where('reserved_until', '>', now()) + ->exists(); + } +} diff --git a/app/Services/ShopService.php b/app/Services/ShopService.php new file mode 100644 index 0000000..cab49bc --- /dev/null +++ b/app/Services/ShopService.php @@ -0,0 +1,222 @@ +jjb < $item->price) { + return ['ok' => false, 'message' => "金币不足,购买 [{$item->name}] 需要 {$item->price} 金币,当前仅有 {$user->jjb} 金币。"]; + } + + return match ($item->type) { + 'instant' => $this->buyInstant($user, $item), + 'duration' => $this->buyWeekCard($user, $item), + 'one_time' => $this->buyRenameCard($user, $item), + default => ['ok' => false, 'message' => '未知商品类型'], + }; + } + + /** + * 购买单次特效卡:立即扣金币,记录已用,返回需要播放的特效 key + * + * @return array{ok:bool, message:string, play_effect?:string} + */ + public function buyInstant(User $user, ShopItem $item): array + { + DB::transaction(function () use ($user, $item) { + // 扣除金币 + $user->decrement('jjb', $item->price); + // 写入已使用记录(用于统计) + UserPurchase::create([ + 'user_id' => $user->id, + 'shop_item_id' => $item->id, + 'status' => 'used', + 'price_paid' => $item->price, + 'used_at' => Carbon::now(), + ]); + }); + + return [ + 'ok' => true, + 'message' => "购买成功!{$item->icon} {$item->name} 正在为您播放...", + 'play_effect' => $item->effectKey(), // 返回特效 key 让前端立即播放 + ]; + } + + /** + * 购买周卡:取消旧周卡(金币不退),激活新周卡,有效期7天 + * + * @return array{ok:bool, message:string} + */ + public function buyWeekCard(User $user, ShopItem $item): array + { + DB::transaction(function () use ($user, $item) { + // 将所有已激活的周卡标记为 cancelled(金币不退) + UserPurchase::where('user_id', $user->id) + ->where('status', 'active') + ->whereHas('shopItem', fn ($q) => $q->where('type', 'duration')) + ->update(['status' => 'cancelled']); + + // 扣除金币 + $user->decrement('jjb', $item->price); + + // 写入新的激活记录 + UserPurchase::create([ + 'user_id' => $user->id, + 'shop_item_id' => $item->id, + 'status' => 'active', + 'price_paid' => $item->price, + 'expires_at' => Carbon::now()->addDays($item->duration_days ?? 7), + ]); + }); + + return ['ok' => true, 'message' => "购买成功!{$item->icon} {$item->name} 已激活,下次登录自动生效(连续7天)。"]; + } + + /** + * 购买改名卡:扣金币、写 active 记录备用 + * + * @return array{ok:bool, message:string} + */ + public function buyRenameCard(User $user, ShopItem $item): array + { + // 检查是否已有未使用的改名卡 + $existing = UserPurchase::where('user_id', $user->id) + ->where('status', 'active') + ->whereHas('shopItem', fn ($q) => $q->where('slug', 'rename_card')) + ->exists(); + + if ($existing) { + return ['ok' => false, 'message' => '您已有一张未使用的改名卡,使用后再购买。']; + } + + DB::transaction(function () use ($user, $item) { + $user->decrement('jjb', $item->price); + UserPurchase::create([ + 'user_id' => $user->id, + 'shop_item_id' => $item->id, + 'status' => 'active', + 'price_paid' => $item->price, + ]); + }); + + return ['ok' => true, 'message' => '改名卡购买成功!请在商店中使用改名卡修改昵称。']; + } + + /** + * 使用改名卡:校验新名、加黑名单、更新用户名 + * + * @param string $newName 新昵称 + * @return array{ok:bool, message:string} + */ + public function useRenameCard(User $user, string $newName): array + { + $newName = trim($newName); + + // 格式校验:1-10字符,中英文数字下划线 + if (! preg_match('/^[\x{4e00}-\x{9fa5}a-zA-Z0-9_]{1,10}$/u', $newName)) { + return ['ok' => false, 'message' => '新昵称格式不合法(1-10字,中英文/数字/下划线)。']; + } + + // 不能与自己现有名相同 + if ($newName === $user->username) { + return ['ok' => false, 'message' => '新昵称与当前昵称相同,请重新输入。']; + } + + // 不能与其他用户重名 + if (User::where('username', $newName)->exists()) { + return ['ok' => false, 'message' => '该昵称已被他人注册,请换一个。']; + } + + // 不能在黑名单保留期内 + if (UsernameBlacklist::isReserved($newName)) { + return ['ok' => false, 'message' => '该昵称处于保护期,暂时无法使用。']; + } + + // 查找有效的改名卡记录 + $purchase = UserPurchase::where('user_id', $user->id) + ->where('status', 'active') + ->whereHas('shopItem', fn ($q) => $q->where('slug', 'rename_card')) + ->first(); + + if (! $purchase) { + return ['ok' => false, 'message' => '您没有可用的改名卡,请先购买。']; + } + + DB::transaction(function () use ($user, $purchase, $newName) { + $oldName = $user->username; + + // 修改用户名 + $user->username = $newName; + $user->save(); + + // 旧名入黑名单(保留30天) + UsernameBlacklist::updateOrCreate( + ['username' => $oldName], + ['reserved_until' => Carbon::now()->addDays(30), 'created_at' => Carbon::now()] + ); + + // 标记改名卡为已使用 + $purchase->update(['status' => 'used', 'used_at' => Carbon::now()]); + }); + + return ['ok' => true, 'message' => "改名成功!您的新昵称为【{$newName}】,旧昵称将保留30天黑名单。注意:历史消息中的旧名不会同步修改。"]; + } + + /** + * 获取用户当前激活的周卡特效 key(如 fireworks/rain/lightning/snow) + * 过期的周卡会自动标记为 expired + */ + public function getActiveWeekEffect(User $user): ?string + { + $purchase = UserPurchase::with('shopItem') + ->where('user_id', $user->id) + ->where('status', 'active') + ->whereHas('shopItem', fn ($q) => $q->where('type', 'duration')) + ->first(); + + if (! $purchase) { + return null; + } + + // 检查是否过期 + if ($purchase->expires_at && $purchase->expires_at->isPast()) { + $purchase->update(['status' => 'expired']); + + return null; + } + + return $purchase->shopItem->effectKey(); + } + + /** + * 获取用户当前激活的改名卡(是否持有未用改名卡) + */ + public function hasRenameCard(User $user): bool + { + return UserPurchase::where('user_id', $user->id) + ->where('status', 'active') + ->whereHas('shopItem', fn ($q) => $q->where('slug', 'rename_card')) + ->exists(); + } +} diff --git a/database/migrations/2026_02_27_074855_create_shop_items_table.php b/database/migrations/2026_02_27_074855_create_shop_items_table.php new file mode 100644 index 0000000..6a00345 --- /dev/null +++ b/database/migrations/2026_02_27_074855_create_shop_items_table.php @@ -0,0 +1,44 @@ +id(); + $table->string('name', 50)->comment('商品名称'); + $table->string('slug', 50)->unique()->comment('商品唯一标识'); + $table->string('description', 255)->nullable()->comment('商品描述'); + $table->string('icon', 20)->default('🛍')->comment('商品图标 emoji'); + $table->unsignedInteger('price')->comment('价格(金币)'); + // type: instant=单次立即执行, duration=有时效, one_time=一次性道具 + $table->enum('type', ['instant', 'duration', 'one_time'])->comment('商品类型'); + $table->unsignedTinyInteger('duration_days')->nullable()->comment('有效天数(duration 类型用)'); + $table->unsignedTinyInteger('sort_order')->default(0)->comment('排序'); + $table->boolean('is_active')->default(true)->comment('是否上架'); + $table->timestamps(); + }); + } + + /** + * 回滚迁移:删除商品表 + */ + public function down(): void + { + Schema::dropIfExists('shop_items'); + } +}; diff --git a/database/migrations/2026_02_27_074855_create_user_purchases_table.php b/database/migrations/2026_02_27_074855_create_user_purchases_table.php new file mode 100644 index 0000000..30c69dd --- /dev/null +++ b/database/migrations/2026_02_27_074855_create_user_purchases_table.php @@ -0,0 +1,49 @@ +id(); + $table->unsignedBigInteger('user_id')->comment('购买用户 ID'); + $table->unsignedBigInteger('shop_item_id')->comment('商品 ID'); + // status: + // active = 有效中(周卡 / 改名卡未使用) + // expired = 已过期(周卡到期) + // used = 已使用(单次卡已播 / 改名卡已用) + // cancelled = 被新购买的周卡覆盖作废(金币不退) + $table->enum('status', ['active', 'expired', 'used', 'cancelled'])->default('active'); + $table->unsignedInteger('price_paid')->comment('购买时实际扣除金币'); + $table->timestamp('expires_at')->nullable()->comment('到期时间(duration 类型使用)'); + $table->timestamp('used_at')->nullable()->comment('使用时间(one_time/instant 使用)'); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('shop_item_id')->references('id')->on('shop_items')->onDelete('cascade'); + $table->index(['user_id', 'status']); + }); + } + + /** + * 回滚迁移:删除购买记录表 + */ + public function down(): void + { + Schema::dropIfExists('user_purchases'); + } +}; diff --git a/database/migrations/2026_02_27_074856_create_username_blacklist_table.php b/database/migrations/2026_02_27_074856_create_username_blacklist_table.php new file mode 100644 index 0000000..fcdd8c5 --- /dev/null +++ b/database/migrations/2026_02_27_074856_create_username_blacklist_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('username', 20)->unique()->comment('被保留的旧用户名'); + $table->timestamp('reserved_until')->comment('保留到期时间(默认30天)'); + $table->timestamp('created_at')->nullable(); + }); + } + + /** + * 回滚迁移:删除黑名单表 + */ + public function down(): void + { + Schema::dropIfExists('username_blacklist'); + } +}; diff --git a/database/seeders/ShopItemSeeder.php b/database/seeders/ShopItemSeeder.php new file mode 100644 index 0000000..945f953 --- /dev/null +++ b/database/seeders/ShopItemSeeder.php @@ -0,0 +1,61 @@ + '烟花单次卡', 'slug' => 'once_fireworks', 'icon' => '🎆', + 'description' => '购买即刻在聊天室绽放一场烟花(仅自己可见),喜庆氛围拉满!', + 'price' => 888, 'type' => 'instant', 'duration_days' => null, 'sort_order' => 1], + ['name' => '下雨单次卡', 'slug' => 'once_rain', 'icon' => '🌧', + 'description' => '立刻召唤一场滂沱大雨,感受诗意雨天(仅自己可见)。', + 'price' => 888, 'type' => 'instant', 'duration_days' => null, 'sort_order' => 2], + ['name' => '雷电单次卡', 'slug' => 'once_lightning', 'icon' => '⚡', + 'description' => '电闪雷鸣,瞬间震撼全场!立即触发雷电特效(仅自己可见)。', + 'price' => 888, 'type' => 'instant', 'duration_days' => null, 'sort_order' => 3], + ['name' => '下雪单次卡', 'slug' => 'once_snow', 'icon' => '❄️', + 'description' => '银装素裹,漫天飞雪!立即触发下雪特效(仅自己可见)。', + 'price' => 888, 'type' => 'instant', 'duration_days' => null, 'sort_order' => 4], + + // ── 周卡(登录自动播放7天,8888金币)────────────────── + ['name' => '烟花周卡', 'slug' => 'week_fireworks', 'icon' => '🎆', + 'description' => '连续7天,每次进入聊天室自动绽放烟花!同时只能激活一种周卡。', + 'price' => 8888, 'type' => 'duration', 'duration_days' => 7, 'sort_order' => 11], + ['name' => '下雨周卡', 'slug' => 'week_rain', 'icon' => '🌧', + 'description' => '连续7天,每次进入聊天室自动下雨。同时只能激活一种周卡。', + 'price' => 8888, 'type' => 'duration', 'duration_days' => 7, 'sort_order' => 12], + ['name' => '雷电周卡', 'slug' => 'week_lightning', 'icon' => '⚡', + 'description' => '连续7天,每次进入聊天室自动触发雷电特效。同时只能激活一种周卡。', + 'price' => 8888, 'type' => 'duration', 'duration_days' => 7, 'sort_order' => 13], + ['name' => '下雪周卡', 'slug' => 'week_snow', 'icon' => '❄️', + 'description' => '连续7天,每次进入聊天室自动下雪。同时只能激活一种周卡。', + 'price' => 8888, 'type' => 'duration', 'duration_days' => 7, 'sort_order' => 14], + + // ── 改名卡(一次性,5000金币)──────────────────────── + ['name' => '改名卡', 'slug' => 'rename_card', 'icon' => '🎭', + 'description' => '使用后可修改一次昵称(旧名保留30天黑名单,不可被他人注册)。注意:历史聊天记录中的旧名不会更改。', + 'price' => 5000, 'type' => 'one_time', 'duration_days' => null, 'sort_order' => 20], + ]; + + foreach ($items as $item) { + ShopItem::firstOrCreate(['slug' => $item['slug']], $item + ['is_active' => true]); + } + } +} diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php index d576ac1..ad46620 100644 --- a/resources/views/chat/frame.blade.php +++ b/resources/views/chat/frame.blade.php @@ -103,6 +103,17 @@ @include('chat.partials.scripts') + {{-- 周卡特效:登录时自动播放(仅持卡用户可见) --}} + @if (!empty($weekEffect)) + + @endif diff --git a/resources/views/chat/partials/right-panel.blade.php b/resources/views/chat/partials/right-panel.blade.php index 3df09dd..efb6d5e 100644 --- a/resources/views/chat/partials/right-panel.blade.php +++ b/resources/views/chat/partials/right-panel.blade.php @@ -5,13 +5,15 @@ 依赖变量:$room(当前房间模型) --}} -
- {{-- Tab 标题栏(原版:名单/房间/贴图/酷库) --}} +
+ {{-- Tab 标题栏 --}}
+
{{-- 用户列表面板 --}} @@ -88,4 +90,7 @@
在线: 0
+ + {{-- 商店面板(位于最后,默认隐藏) --}} + @include('chat.partials.shop-panel')
diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index f1b7342..49aa15b 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -27,10 +27,20 @@ // ── Tab 切换 ────────────────────────────────────── function switchTab(tab) { + // 所有内容面板(名单/房间/贴图/酷库 用 block,商店用 flex) ['users', 'rooms', 'emoji', 'action'].forEach(t => { document.getElementById('panel-' + t).style.display = t === tab ? 'block' : 'none'; document.getElementById('tab-' + t).classList.toggle('active', t === tab); }); + // 商店面板单独处理(flex 布局以支持内部滚动) + const shopPanel = document.getElementById('shop-panel'); + const shopTab = document.getElementById('tab-shop'); + if (shopPanel && shopTab) { + shopPanel.style.display = tab === 'shop' ? 'flex' : 'none'; + shopPanel.style.flexDirection = 'column'; + shopPanel.style.height = '100%'; + shopTab.classList.toggle('active', tab === 'shop'); + } // 贴图 Tab 懒加载:首次切换时将 data-src 赋值到 src if (tab === 'emoji') { document.querySelectorAll('#panel-emoji img[data-src]').forEach(img => { diff --git a/resources/views/chat/partials/shop-panel.blade.php b/resources/views/chat/partials/shop-panel.blade.php new file mode 100644 index 0000000..ae10629 --- /dev/null +++ b/resources/views/chat/partials/shop-panel.blade.php @@ -0,0 +1,273 @@ +{{-- + 文件功能:商店面板视图(嵌入聊天室右侧) + 展示单次卡、周卡、改名卡,支持购买和改名操作 +--}} + + + + diff --git a/routes/web.php b/routes/web.php index caba8ce..dbacd4f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -100,6 +100,11 @@ Route::middleware(['chat.auth'])->group(function () { Route::post('/command/announce', [AdminCommandController::class, 'announce'])->name('command.announce'); Route::post('/command/clear-screen', [AdminCommandController::class, 'clearScreen'])->name('command.clear_screen'); Route::post('/command/effect', [AdminCommandController::class, 'effect'])->name('command.effect'); + + // ---- 商店(购买特效卡/改名卡)---- + Route::get('/shop/items', [\App\Http\Controllers\ShopController::class, 'items'])->name('shop.items'); + Route::post('/shop/buy', [\App\Http\Controllers\ShopController::class, 'buy'])->name('shop.buy'); + Route::post('/shop/rename', [\App\Http\Controllers\ShopController::class, 'rename'])->name('shop.rename'); }); // 强力特权层中间件:同时验证 chat.auth 登录态 和 chat.level:super 特权(superlevel 由 sysparam 配置)