diff --git a/app/Console/Commands/AutoSaveExp.php b/app/Console/Commands/AutoSaveExp.php index 1af9475..6e02380 100644 --- a/app/Console/Commands/AutoSaveExp.php +++ b/app/Console/Commands/AutoSaveExp.php @@ -304,11 +304,11 @@ class AutoSaveExp extends Command : now(); PositionDutyLog::create([ - 'user_id' => $user->id, + 'user_id' => $user->id, 'user_position_id' => $activeUP->id, - 'login_at' => $loginAt, - 'ip_address' => '0.0.0.0', - 'room_id' => $roomId, + 'login_at' => $loginAt, + 'ip_address' => '0.0.0.0', + 'room_id' => $roomId, ]); } } diff --git a/app/Console/Commands/ClearRoomOnlineCache.php b/app/Console/Commands/ClearRoomOnlineCache.php index bbf385d..c03dc47 100644 --- a/app/Console/Commands/ClearRoomOnlineCache.php +++ b/app/Console/Commands/ClearRoomOnlineCache.php @@ -39,8 +39,8 @@ class ClearRoomOnlineCache extends Command */ public function handle(): int { - $prefix = config('database.redis.options.prefix', ''); - $cursor = '0'; + $prefix = config('database.redis.options.prefix', ''); + $cursor = '0'; $cleaned = 0; $this->info("Redis 前缀:\"{$prefix}\""); diff --git a/app/Events/RedPacketClaimed.php b/app/Events/RedPacketClaimed.php index ecec58c..a3fc412 100644 --- a/app/Events/RedPacketClaimed.php +++ b/app/Events/RedPacketClaimed.php @@ -7,6 +7,7 @@ * 前端收到后弹出 Toast 通知展示到账金额。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ @@ -24,9 +25,9 @@ class RedPacketClaimed implements ShouldBroadcastNow use Dispatchable, InteractsWithSockets, SerializesModels; /** - * @param User $claimer 领取用户 - * @param int $amount 领取金额 - * @param int $envelopeId 红包 ID + * @param User $claimer 领取用户 + * @param int $amount 领取金额 + * @param int $envelopeId 红包 ID */ public function __construct( public readonly User $claimer, @@ -51,8 +52,8 @@ class RedPacketClaimed implements ShouldBroadcastNow { return [ 'envelope_id' => $this->envelopeId, - 'amount' => $this->amount, - 'message' => "🧧 成功抢到 {$this->amount} 金币礼包!", + 'amount' => $this->amount, + 'message' => "🧧 成功抢到 {$this->amount} 金币礼包!", ]; } diff --git a/app/Events/RedPacketSent.php b/app/Events/RedPacketSent.php index ca1eab1..a33db3d 100644 --- a/app/Events/RedPacketSent.php +++ b/app/Events/RedPacketSent.php @@ -7,6 +7,7 @@ * 前端接收后显示红包卡片弹窗,并在聊天窗口追加系统公告。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ @@ -23,20 +24,20 @@ class RedPacketSent implements ShouldBroadcastNow use Dispatchable, InteractsWithSockets, SerializesModels; /** - * @param int $roomId 房间 ID - * @param int $envelopeId 红包 ID - * @param string $senderUsername 发包人用户名 - * @param int $totalAmount 总金额(金币) - * @param int $totalCount 总份数 - * @param int $expireSeconds 过期秒数(用于前端倒计时) + * @param int $roomId 房间 ID + * @param int $envelopeId 红包 ID + * @param string $senderUsername 发包人用户名 + * @param int $totalAmount 总金额(金币) + * @param int $totalCount 总份数 + * @param int $expireSeconds 过期秒数(用于前端倒计时) */ public function __construct( - public readonly int $roomId, - public readonly int $envelopeId, + public readonly int $roomId, + public readonly int $envelopeId, public readonly string $senderUsername, - public readonly int $totalAmount, - public readonly int $totalCount, - public readonly int $expireSeconds, + public readonly int $totalAmount, + public readonly int $totalCount, + public readonly int $expireSeconds, public readonly string $type = 'gold', ) {} @@ -58,12 +59,12 @@ class RedPacketSent implements ShouldBroadcastNow public function broadcastWith(): array { return [ - 'envelope_id' => $this->envelopeId, + 'envelope_id' => $this->envelopeId, 'sender_username' => $this->senderUsername, - 'total_amount' => $this->totalAmount, - 'total_count' => $this->totalCount, - 'expire_seconds' => $this->expireSeconds, - 'type' => $this->type, + 'total_amount' => $this->totalAmount, + 'total_count' => $this->totalCount, + 'expire_seconds' => $this->expireSeconds, + 'type' => $this->type, ]; } diff --git a/app/Http/Controllers/Admin/CurrencyStatsController.php b/app/Http/Controllers/Admin/CurrencyStatsController.php index 9628b80..e0ce4fb 100644 --- a/app/Http/Controllers/Admin/CurrencyStatsController.php +++ b/app/Http/Controllers/Admin/CurrencyStatsController.php @@ -6,6 +6,7 @@ * 仅限 superlevel 以上管理员访问。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ @@ -46,14 +47,14 @@ class CurrencyStatsController extends Controller // 今日净流通量(正向增加 - 负向消耗),可判断通货膨胀 $netFlow = []; foreach (['exp', 'gold', 'charm'] as $currency) { - $totalIn = UserCurrencyLog::whereDate('created_at', $date) + $totalIn = UserCurrencyLog::whereDate('created_at', $date) ->where('currency', $currency)->where('amount', '>', 0) ->sum('amount'); $totalOut = UserCurrencyLog::whereDate('created_at', $date) ->where('currency', $currency)->where('amount', '<', 0) ->sum('amount'); $netFlow[$currency] = [ - 'in' => $totalIn, + 'in' => $totalIn, 'out' => abs($totalOut), 'net' => $totalIn + $totalOut, // 净增量 ]; diff --git a/app/Http/Controllers/Admin/FishingEventController.php b/app/Http/Controllers/Admin/FishingEventController.php index 0238f22..b322028 100644 --- a/app/Http/Controllers/Admin/FishingEventController.php +++ b/app/Http/Controllers/Admin/FishingEventController.php @@ -25,7 +25,7 @@ class FishingEventController extends Controller */ public function index(): View { - $events = FishingEvent::orderBy('sort')->orderBy('id')->get(); + $events = FishingEvent::orderBy('sort')->orderBy('id')->get(); $totalWeight = $events->where('is_active', true)->sum('weight'); return view('admin.fishing.index', compact('events', 'totalWeight')); @@ -37,13 +37,13 @@ class FishingEventController extends Controller public function store(Request $request): RedirectResponse { $data = $request->validate([ - 'emoji' => 'required|string|max:10', - 'name' => 'required|string|max:100', - 'message' => 'required|string|max:255', - 'exp' => 'required|integer', - 'jjb' => 'required|integer', - 'weight' => 'required|integer|min:1|max:9999', - 'sort' => 'required|integer|min:0', + 'emoji' => 'required|string|max:10', + 'name' => 'required|string|max:100', + 'message' => 'required|string|max:255', + 'exp' => 'required|integer', + 'jjb' => 'required|integer', + 'weight' => 'required|integer|min:1|max:9999', + 'sort' => 'required|integer|min:0', 'is_active' => 'boolean', ]); @@ -61,13 +61,13 @@ class FishingEventController extends Controller public function update(Request $request, FishingEvent $fishing): RedirectResponse { $data = $request->validate([ - 'emoji' => 'required|string|max:10', - 'name' => 'required|string|max:100', - 'message' => 'required|string|max:255', - 'exp' => 'required|integer', - 'jjb' => 'required|integer', - 'weight' => 'required|integer|min:1|max:9999', - 'sort' => 'required|integer|min:0', + 'emoji' => 'required|string|max:10', + 'name' => 'required|string|max:100', + 'message' => 'required|string|max:255', + 'exp' => 'required|integer', + 'jjb' => 'required|integer', + 'weight' => 'required|integer|min:1|max:9999', + 'sort' => 'required|integer|min:0', 'is_active' => 'boolean', ]); @@ -87,9 +87,9 @@ class FishingEventController extends Controller $fishing->update(['is_active' => ! $fishing->is_active]); return response()->json([ - 'ok' => true, + 'ok' => true, 'is_active' => $fishing->is_active, - 'message' => $fishing->is_active ? "「{$fishing->name}」已启用" : "「{$fishing->name}」已禁用", + 'message' => $fishing->is_active ? "「{$fishing->name}」已启用" : "「{$fishing->name}」已禁用", ]); } diff --git a/app/Http/Controllers/Admin/OpsController.php b/app/Http/Controllers/Admin/OpsController.php index ea8cb24..49e0549 100644 --- a/app/Http/Controllers/Admin/OpsController.php +++ b/app/Http/Controllers/Admin/OpsController.php @@ -88,8 +88,8 @@ class OpsController extends Controller abort(403, '无权限操作'); } - $prefix = config('database.redis.options.prefix', ''); - $cursor = '0'; + $prefix = config('database.redis.options.prefix', ''); + $cursor = '0'; $cleaned = 0; do { diff --git a/app/Http/Controllers/Admin/RoomManagerController.php b/app/Http/Controllers/Admin/RoomManagerController.php index 0a47826..c8cf2cf 100644 --- a/app/Http/Controllers/Admin/RoomManagerController.php +++ b/app/Http/Controllers/Admin/RoomManagerController.php @@ -36,17 +36,17 @@ class RoomManagerController extends Controller public function store(Request $request): RedirectResponse { $data = $request->validate([ - 'room_name' => 'required|string|max:100|unique:rooms,room_name', - 'room_des' => 'nullable|string|max:500', - 'room_owner' => 'nullable|string|max:50', + 'room_name' => 'required|string|max:100|unique:rooms,room_name', + 'room_des' => 'nullable|string|max:500', + 'room_owner' => 'nullable|string|max:50', 'permit_level' => 'required|integer|min:0|max:15', - 'door_open' => 'required|boolean', + 'door_open' => 'required|boolean', ], [ 'room_name.unique' => '房间名称已存在,请换一个名称。', ]); // 设置新建房间的默认值 - $data['room_keep'] = false; // 新建房间均为非系统房间,可删除 + $data['room_keep'] = false; // 新建房间均为非系统房间,可删除 $data['build_time'] = now(); $room = Room::create($data); @@ -62,12 +62,12 @@ class RoomManagerController extends Controller public function update(Request $request, Room $room): RedirectResponse { $data = $request->validate([ - 'room_name' => 'required|string|max:100', - 'room_des' => 'nullable|string|max:500', + 'room_name' => 'required|string|max:100', + 'room_des' => 'nullable|string|max:500', 'announcement' => 'nullable|string|max:500', - 'room_owner' => 'nullable|string|max:50', + 'room_owner' => 'nullable|string|max:50', 'permit_level' => 'required|integer|min:0|max:15', - 'door_open' => 'required|boolean', + 'door_open' => 'required|boolean', ]); $room->update($data); diff --git a/app/Http/Controllers/Admin/SmtpController.php b/app/Http/Controllers/Admin/SmtpController.php index 9805b90..4002bf5 100644 --- a/app/Http/Controllers/Admin/SmtpController.php +++ b/app/Http/Controllers/Admin/SmtpController.php @@ -65,7 +65,7 @@ class SmtpController extends Controller public function test(Request $request): RedirectResponse { $request->validate([ - 'test_email' => 'required|email' + 'test_email' => 'required|email', ]); $testEmail = $request->input('test_email'); @@ -78,7 +78,7 @@ class SmtpController extends Controller return redirect()->route('admin.smtp.edit')->with('success', "测试邮件已成功发送至 {$testEmail},请注意查收。"); } catch (\Throwable $e) { - return redirect()->route('admin.smtp.edit')->with('error', "测试发出失败,原因:" . $e->getMessage()); + return redirect()->route('admin.smtp.edit')->with('error', '测试发出失败,原因:'.$e->getMessage()); } } } diff --git a/app/Http/Controllers/Api/VerificationController.php b/app/Http/Controllers/Api/VerificationController.php index d1b7e9d..0e9681b 100644 --- a/app/Http/Controllers/Api/VerificationController.php +++ b/app/Http/Controllers/Api/VerificationController.php @@ -17,7 +17,7 @@ class VerificationController extends Controller public function sendEmailCode(Request $request): JsonResponse { $request->validate([ - 'email' => 'required|email' + 'email' => 'required|email', ]); $email = $request->input('email'); @@ -27,23 +27,24 @@ class VerificationController extends Controller if (SysParam::where('alias', 'smtp_enabled')->value('body') !== '1') { return response()->json([ 'status' => 'error', - 'message' => '抱歉,当前系统未开启外部邮件发信服务,请联系管理员。' + 'message' => '抱歉,当前系统未开启外部邮件发信服务,请联系管理员。', ], 403); } // 2. 检查是否有频率限制(同一用户或同一邮箱,60秒只允许发1次) - $throttleKey = 'email_throttle_' . $user->id; + $throttleKey = 'email_throttle_'.$user->id; if (Cache::has($throttleKey)) { $ttl = Cache::ttl($throttleKey); + return response()->json([ 'status' => 'error', - 'message' => "发送过于频繁,请等待 {$ttl} 秒后再试。" + 'message' => "发送过于频繁,请等待 {$ttl} 秒后再试。", ], 429); } // 3. 生成 6 位随机验证码并缓存,有效期 5 分钟 $code = mt_rand(100000, 999999); - $codeKey = 'email_verify_code_' . $user->id . '_' . $email; + $codeKey = 'email_verify_code_'.$user->id.'_'.$email; Cache::put($codeKey, $code, now()->addMinutes(5)); // 设置频率锁,过期时间 60 秒 @@ -57,14 +58,15 @@ class VerificationController extends Controller return response()->json([ 'status' => 'success', - 'message' => '验证码已发送,请注意查收邮件。' + 'message' => '验证码已发送,请注意查收邮件。', ]); } catch (\Throwable $e) { // 如果发信失败,主动接触频率限制锁方便用户下一次立重试 Cache::forget($throttleKey); + return response()->json([ 'status' => 'error', - 'message' => '邮件系统发送异常,请稍后再试: ' . $e->getMessage() + 'message' => '邮件系统发送异常,请稍后再试: '.$e->getMessage(), ], 500); } } diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php index 9f6a965..784698e 100644 --- a/app/Http/Controllers/ChatController.php +++ b/app/Http/Controllers/ChatController.php @@ -35,7 +35,10 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Redis; +use Illuminate\Support\Facades\Storage; use Illuminate\View\View; +use Intervention\Image\Drivers\Gd\Driver; +use Intervention\Image\ImageManager; class ChatController extends Controller { @@ -683,9 +686,12 @@ class ChatController extends Controller return response()->json(['status' => 'error', 'message' => '头像文件不存在'], 422); } - // 更新用户头像 - $user->usersf = $headface; - $user->save(); + // 更新前如为自定义头像,将其从磁盘删除,节约空间 + if ($user->usersf !== $headface) { + $user->deleteCustomAvatar(); + $user->usersf = $headface; + $user->save(); + } // 将新头像同步到 Redis 在线用户列表中(所有房间) // 通过更新 Redis 的用户信息,使得其他用户和自己刷新后都能看到新头像 @@ -710,6 +716,71 @@ class ChatController extends Controller ]); } + /** + * 上传自定义头像 + */ + public function uploadAvatar(Request $request): JsonResponse + { + $request->validate([ + 'file' => 'required|image|mimes:jpeg,png,jpg,gif,webp|max:2048', + ]); + + $user = Auth::user(); + if (! $user) { + return response()->json(['status' => 'error', 'message' => '未登录'], 401); + } + + $file = $request->file('file'); + + try { + $manager = new ImageManager(new Driver); + $image = $manager->read($file); + + // 裁剪正方形并压缩为 112x112 + $image->cover(112, 112); + + // 生成相对路径 + $filename = 'custom_'.$user->id.'_'.time().'.'.$file->extension(); + $path = 'avatars/'.$filename; + + // 保存以高质量 JPG 或原格式 + Storage::disk('public')->put($path, (string) $image->encode()); + + $dbValue = 'storage/'.$path; + + // 更新前如为自定义头像,将其从磁盘删除,节约空间 + if ($user->usersf !== $dbValue) { + $user->deleteCustomAvatar(); + $user->usersf = $dbValue; + $user->save(); + } + + // 同步 Redis 状态 + $superLevel = (int) Sysparam::getValue('superlevel', '100'); + $rooms = $this->chatState->getUserRooms($user->username); + foreach ($rooms as $roomId) { + $this->chatState->userJoin((int) $roomId, $user->username, [ + 'level' => $user->user_level, + 'sex' => $user->sex, + 'headface' => $user->headface, // Use accessor + 'vip_icon' => $user->vipIcon(), + 'vip_name' => $user->vipName(), + 'vip_color' => $user->isVip() ? ($user->vipLevel?->color ?? '') : '', + 'is_admin' => $user->user_level >= $superLevel, + ]); + } + + return response()->json([ + 'status' => 'success', + 'message' => '头像上传成功!', + 'headface' => $user->headface, + ]); + + } catch (\Exception $e) { + return response()->json(['status' => 'error', 'message' => '上传失败: '.$e->getMessage()], 500); + } + } + /** * 设置房间公告/祝福语(滚动显示在聊天室顶部) * 需要房间主人或等级达到 level_announcement 配置值 diff --git a/app/Http/Controllers/FishingController.php b/app/Http/Controllers/FishingController.php index f1dd25c..045ed28 100644 --- a/app/Http/Controllers/FishingController.php +++ b/app/Http/Controllers/FishingController.php @@ -94,8 +94,8 @@ class FishingController extends Controller $user->refresh(); // 4. 生成一次性 token,存入 Redis(TTL = 等待时间 + 收竿窗口 + 缓冲) - $waitMin = (int) (GameConfig::param('fishing', 'fishing_wait_min') ?? Sysparam::getValue('fishing_wait_min', '8')); - $waitMax = (int) (GameConfig::param('fishing', 'fishing_wait_max') ?? Sysparam::getValue('fishing_wait_max', '15')); + $waitMin = (int) (GameConfig::param('fishing', 'fishing_wait_min') ?? Sysparam::getValue('fishing_wait_min', '8')); + $waitMax = (int) (GameConfig::param('fishing', 'fishing_wait_max') ?? Sysparam::getValue('fishing_wait_max', '15')); $waitTime = rand($waitMin, $waitMax); $token = Str::random(32); $tokenKey = "fishing:token:{$user->id}"; @@ -249,18 +249,18 @@ class FishingController extends Controller // 数据库无事件时的兜底 if (! $event) { return [ - 'emoji' => '🐟', + 'emoji' => '🐟', 'message' => '钓到一条小鱼,获得金币10', - 'exp' => 0, - 'jjb' => 10, + 'exp' => 0, + 'jjb' => 10, ]; } return [ - 'emoji' => $event->emoji, + 'emoji' => $event->emoji, 'message' => $event->message, - 'exp' => $event->exp, - 'jjb' => $event->jjb, + 'exp' => $event->exp, + 'jjb' => $event->jjb, ]; } } diff --git a/app/Http/Controllers/FriendController.php b/app/Http/Controllers/FriendController.php index a8bf60b..4ad6bf5 100644 --- a/app/Http/Controllers/FriendController.php +++ b/app/Http/Controllers/FriendController.php @@ -201,13 +201,13 @@ class FriendController extends Controller $row = $myRows->get($u->username); return [ - 'username' => $u->username, - 'headface' => $u->headface, + 'username' => $u->username, + 'headface' => $u->headface, 'user_level' => $u->user_level, - 'sex' => $u->sex, - 'mutual' => $addedMeNames->contains($u->username), // 是否互相添加 - 'sub_time' => $row?->sub_time?->format('Y-m-d H:i') ?? '', - 'is_online' => $onlineUsernames->contains($u->username), + 'sex' => $u->sex, + 'mutual' => $addedMeNames->contains($u->username), // 是否互相添加 + 'sub_time' => $row?->sub_time?->format('Y-m-d H:i') ?? '', + 'is_online' => $onlineUsernames->contains($u->username), ]; }) ->sortByDesc('is_online') // 在线好友排在前面 @@ -221,19 +221,19 @@ class FriendController extends Controller $row = $addedMeRows->get($u->username); return [ - 'username' => $u->username, - 'headface' => $u->headface, + 'username' => $u->username, + 'headface' => $u->headface, 'user_level' => $u->user_level, - 'sex' => $u->sex, - 'added_at' => $row?->sub_time?->format('Y-m-d H:i') ?? '', - 'is_online' => $onlineUsernames->contains($u->username), + 'sex' => $u->sex, + 'added_at' => $row?->sub_time?->format('Y-m-d H:i') ?? '', + 'is_online' => $onlineUsernames->contains($u->username), ]; }) ->sortByDesc('is_online') ->values(); return response()->json([ - 'status' => 'success', + 'status' => 'success', 'friends' => $friends, 'pending' => $pending, ]); diff --git a/app/Http/Controllers/LeaderboardController.php b/app/Http/Controllers/LeaderboardController.php index 5d8f4ff..a57368f 100644 --- a/app/Http/Controllers/LeaderboardController.php +++ b/app/Http/Controllers/LeaderboardController.php @@ -86,12 +86,12 @@ class LeaderboardController extends Controller // ── 今日榜(5分钟缓存,数据来自 user_currency_logs 流水表)── $todayTtl = 60 * 5; - $today = today()->toDateString(); + $today = today()->toDateString(); - $todayExp = Cache::remember("leaderboard:today_exp:{$today}", $todayTtl, + $todayExp = Cache::remember("leaderboard:today_exp:{$today}", $todayTtl, fn () => $this->currencyService->todayLeaderboard('exp', $topN, $today) ); - $todayGold = Cache::remember("leaderboard:today_gold:{$today}", $todayTtl, + $todayGold = Cache::remember("leaderboard:today_gold:{$today}", $todayTtl, fn () => $this->currencyService->todayLeaderboard('gold', $topN, $today) ); $todayCharm = Cache::remember("leaderboard:today_charm:{$today}", $todayTtl, @@ -109,13 +109,13 @@ class LeaderboardController extends Controller public function todayIndex(): View { $todayTtl = 60 * 5; - $today = today()->toDateString(); - $topN = (int) \App\Models\Sysparam::getValue('leaderboard_limit', '20'); + $today = today()->toDateString(); + $topN = (int) \App\Models\Sysparam::getValue('leaderboard_limit', '20'); - $todayExp = Cache::remember("leaderboard:today_exp:{$today}", $todayTtl, + $todayExp = Cache::remember("leaderboard:today_exp:{$today}", $todayTtl, fn () => $this->currencyService->todayLeaderboard('exp', $topN, $today) ); - $todayGold = Cache::remember("leaderboard:today_gold:{$today}", $todayTtl, + $todayGold = Cache::remember("leaderboard:today_gold:{$today}", $todayTtl, fn () => $this->currencyService->todayLeaderboard('gold', $topN, $today) ); $todayCharm = Cache::remember("leaderboard:today_charm:{$today}", $todayTtl, @@ -130,10 +130,10 @@ class LeaderboardController extends Controller */ public function myLogs(): View { - $user = auth()->user(); + $user = auth()->user(); $currency = request('currency'); - $days = (int) request('days', 7); - $logs = $this->currencyService->userLogs($user->id, $currency ?: null, $days); + $days = (int) request('days', 7); + $logs = $this->currencyService->userLogs($user->id, $currency ?: null, $days); return view('leaderboard.my-logs', compact('logs', 'user', 'currency', 'days')); } diff --git a/app/Http/Controllers/MysteryBoxController.php b/app/Http/Controllers/MysteryBoxController.php index 6e22142..ce74228 100644 --- a/app/Http/Controllers/MysteryBoxController.php +++ b/app/Http/Controllers/MysteryBoxController.php @@ -15,6 +15,7 @@ * 5. 公屏广播结果(中奖/踩雷) * * @author ChatRoom Laravel + * * @version 1.0.0 */ @@ -58,12 +59,12 @@ class MysteryBoxController extends Controller $secondsLeft = $box->expires_at ? max(0, now()->diffInSeconds($box->expires_at, false)) : null; return response()->json([ - 'active' => true, - 'box_id' => $box->id, - 'box_type' => $box->box_type, - 'type_name' => $box->typeName(), - 'type_emoji' => $box->typeEmoji(), - 'passcode' => $box->passcode, + 'active' => true, + 'box_id' => $box->id, + 'box_type' => $box->box_type, + 'type_name' => $box->typeName(), + 'type_emoji' => $box->typeEmoji(), + 'passcode' => $box->passcode, 'seconds_left' => $secondsLeft, ]); } @@ -105,15 +106,15 @@ class MysteryBoxController extends Controller $source = $reward >= 0 ? CurrencySource::MYSTERY_BOX : CurrencySource::MYSTERY_BOX_TRAP; $remark = $reward >= 0 ? "神秘箱子【{$box->typeName()}】奖励" - : "神秘箱子【黑化箱】陷阱扣除"; + : '神秘箱子【黑化箱】陷阱扣除'; $this->currency->change($user, 'gold', $reward, $source, $remark, $box->room_id); // ③ 写领取记录 + 更新箱子状态 MysteryBoxClaim::create([ 'mystery_box_id' => $box->id, - 'user_id' => $user->id, - 'reward_amount' => $reward, + 'user_id' => $user->id, + 'reward_amount' => $reward, ]); $box->update(['status' => 'claimed']); @@ -123,12 +124,12 @@ class MysteryBoxController extends Controller $this->broadcastResult($box, $user->username, $reward); return response()->json([ - 'ok' => true, - 'reward' => $reward, + 'ok' => true, + 'reward' => $reward, 'balance' => $user->jjb ?? 0, 'message' => $reward >= 0 ? "🎉 恭喜!开箱获得 +{$reward} 金币!" - : "☠️ 中了黑化陷阱!扣除 " . abs($reward) . ' 金币!', + : '☠️ 中了黑化陷阱!扣除 '.abs($reward).' 金币!', ]); }); } @@ -136,9 +137,9 @@ class MysteryBoxController extends Controller /** * 公屏广播开箱结果。 * - * @param MysteryBox $box 箱子实例 - * @param string $username 领取者用户名 - * @param int $reward 奖励金额(正/负) + * @param MysteryBox $box 箱子实例 + * @param string $username 领取者用户名 + * @param int $reward 奖励金额(正/负) */ private function broadcastResult(MysteryBox $box, string $username, int $reward): void { @@ -147,24 +148,24 @@ class MysteryBoxController extends Controller if ($reward >= 0) { $content = "{$emoji}【开箱播报】恭喜 【{$username}】 抢到了神秘{$typeName}!" - . "获得 💰" . number_format($reward) . " 金币!"; + .'获得 💰'.number_format($reward).' 金币!'; $color = $box->box_type === 'rare' ? '#c4b5fd' : '#34d399'; } else { $content = "☠️【黑化陷阱】haha!【{$username}】 中了神秘黑化箱的陷阱!" - . "被扣除 💰" . number_format(abs($reward)) . " 金币!点背~"; + .'被扣除 💰'.number_format(abs($reward)).' 金币!点背~'; $color = '#f87171'; } $msg = [ - 'id' => $this->chatState->nextMessageId(1), - 'room_id' => 1, + 'id' => $this->chatState->nextMessageId(1), + 'room_id' => 1, 'from_user' => '系统传音', - 'to_user' => '大家', - 'content' => $content, + 'to_user' => '大家', + 'content' => $content, 'is_secret' => false, 'font_color' => $color, - 'action' => '大声宣告', - 'sent_at' => now()->toDateTimeString(), + 'action' => '大声宣告', + 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage(1, $msg); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 3bfe1d7..f40aae7 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -170,6 +170,10 @@ class UserController extends Controller \Illuminate\Support\Facades\Cache::forget($codeKey); } + if (isset($data['headface']) && $data['headface'] !== $user->headface) { + $user->deleteCustomAvatar(); + } + $user->update($data); return response()->json(['status' => 'success', 'message' => '资料更新成功。']); diff --git a/app/Jobs/DropMysteryBoxJob.php b/app/Jobs/DropMysteryBoxJob.php index 41ae363..8699358 100644 --- a/app/Jobs/DropMysteryBoxJob.php +++ b/app/Jobs/DropMysteryBoxJob.php @@ -9,6 +9,7 @@ * 3. 设定定时关闭任务(windows_seconds 秒后过期) * * @author ChatRoom Laravel + * * @version 1.0.0 */ @@ -32,8 +33,8 @@ class DropMysteryBoxJob implements ShouldQueue public int $tries = 1; /** - * @param string $boxType 箱子类型:normal | rare | trap - * @param int|null $roomId 投放目标房间,null 时默认 1 + * @param string $boxType 箱子类型:normal | rare | trap + * @param int|null $roomId 投放目标房间,null 时默认 1 * @param string|null $passcode 手动指定暗号(null=自动生成) * @param int|null $droppedBy 投放者用户ID(null=系统自动) */ @@ -79,11 +80,11 @@ class DropMysteryBoxJob implements ShouldQueue // 创建箱子记录 $box = MysteryBox::create([ - 'box_type' => $this->boxType, - 'passcode' => $passcode, + 'box_type' => $this->boxType, + 'passcode' => $passcode, 'reward_min' => $rewardMin, 'reward_max' => $rewardMax, - 'status' => 'open', + 'status' => 'open', 'expires_at' => now()->addSeconds($claimWindow), 'dropped_by' => $this->droppedBy, ]); @@ -94,22 +95,22 @@ class DropMysteryBoxJob implements ShouldQueue $source = $this->droppedBy ? '管理员' : '系统'; $content = "{$emoji}【{$typeName}】{$source}投放了一个神秘箱子!" - . "发送暗号「{$passcode}」即可开箱!限时 {$claimWindow} 秒,先到先得!"; + ."发送暗号「{$passcode}」即可开箱!限时 {$claimWindow} 秒,先到先得!"; $msg = [ - 'id' => $chatState->nextMessageId($targetRoom), - 'room_id' => $targetRoom, - 'from_user' => '系统传音', - 'to_user' => '大家', - 'content' => $content, - 'is_secret' => false, + 'id' => $chatState->nextMessageId($targetRoom), + 'room_id' => $targetRoom, + 'from_user' => '系统传音', + 'to_user' => '大家', + 'content' => $content, + 'is_secret' => false, 'font_color' => match ($this->boxType) { - 'rare' => '#c4b5fd', - 'trap' => '#f87171', + 'rare' => '#c4b5fd', + 'trap' => '#f87171', default => '#34d399', }, - 'action' => '大声宣告', - 'sent_at' => now()->toDateTimeString(), + 'action' => '大声宣告', + 'sent_at' => now()->toDateTimeString(), ]; $chatState->pushMessage($targetRoom, $msg); diff --git a/app/Jobs/ExpireMysteryBoxJob.php b/app/Jobs/ExpireMysteryBoxJob.php index a8d440a..0f747d6 100644 --- a/app/Jobs/ExpireMysteryBoxJob.php +++ b/app/Jobs/ExpireMysteryBoxJob.php @@ -6,6 +6,7 @@ * 在箱子到期后将其状态更新为 expired(若尚未被领取),并向公屏广播过期通知。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ @@ -48,15 +49,15 @@ class ExpireMysteryBoxJob implements ShouldQueue // 公屏广播过期通知 $msg = [ - 'id' => $chatState->nextMessageId(1), - 'room_id' => 1, + 'id' => $chatState->nextMessageId(1), + 'room_id' => 1, 'from_user' => '系统传音', - 'to_user' => '大家', - 'content' => "⏰ 神秘箱子(暗号:{$box->passcode})已超时,箱子消失了!下次要快哦~", + 'to_user' => '大家', + 'content' => "⏰ 神秘箱子(暗号:{$box->passcode})已超时,箱子消失了!下次要快哦~", 'is_secret' => false, 'font_color' => '#9ca3af', - 'action' => '大声宣告', - 'sent_at' => now()->toDateTimeString(), + 'action' => '大声宣告', + 'sent_at' => now()->toDateTimeString(), ]; $chatState->pushMessage(1, $msg); diff --git a/app/Models/FishingEvent.php b/app/Models/FishingEvent.php index a95fbf0..1ab44d0 100644 --- a/app/Models/FishingEvent.php +++ b/app/Models/FishingEvent.php @@ -42,11 +42,11 @@ class FishingEvent extends Model protected function casts(): array { return [ - 'exp' => 'integer', - 'jjb' => 'integer', - 'weight' => 'integer', + 'exp' => 'integer', + 'jjb' => 'integer', + 'weight' => 'integer', 'is_active' => 'boolean', - 'sort' => 'integer', + 'sort' => 'integer', ]; } @@ -73,8 +73,8 @@ class FishingEvent extends Model // 计算总权重后加权随机 $totalWeight = $events->sum('weight'); - $roll = random_int(1, max($totalWeight, 1)); - $cumulative = 0; + $roll = random_int(1, max($totalWeight, 1)); + $cumulative = 0; foreach ($events as $event) { $cumulative += $event->weight; diff --git a/app/Models/MysteryBox.php b/app/Models/MysteryBox.php index 5dc49dc..1eb6e54 100644 --- a/app/Models/MysteryBox.php +++ b/app/Models/MysteryBox.php @@ -7,6 +7,7 @@ * 对应表:mystery_boxes * * @author ChatRoom Laravel + * * @version 1.0.0 */ @@ -81,9 +82,9 @@ class MysteryBox extends Model { return match ($this->box_type) { 'normal' => '📦', - 'rare' => '💎', - 'trap' => '☠️', - default => '📦', + 'rare' => '💎', + 'trap' => '☠️', + default => '📦', }; } @@ -94,9 +95,9 @@ class MysteryBox extends Model { return match ($this->box_type) { 'normal' => '普通箱', - 'rare' => '稀有箱', - 'trap' => '黑化箱', - default => '神秘箱', + 'rare' => '稀有箱', + 'trap' => '黑化箱', + default => '神秘箱', }; } diff --git a/app/Models/MysteryBoxClaim.php b/app/Models/MysteryBoxClaim.php index 8d1b04b..5c0007d 100644 --- a/app/Models/MysteryBoxClaim.php +++ b/app/Models/MysteryBoxClaim.php @@ -7,6 +7,7 @@ * 对应表:mystery_box_claims * * @author ChatRoom Laravel + * * @version 1.0.0 */ diff --git a/app/Models/RedPacketClaim.php b/app/Models/RedPacketClaim.php index 8a3023d..416c3ea 100644 --- a/app/Models/RedPacketClaim.php +++ b/app/Models/RedPacketClaim.php @@ -7,6 +7,7 @@ * envelope_id + user_id 联合唯一约束保证幂等性。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ diff --git a/app/Models/RedPacketEnvelope.php b/app/Models/RedPacketEnvelope.php index 14eef18..93d8d94 100644 --- a/app/Models/RedPacketEnvelope.php +++ b/app/Models/RedPacketEnvelope.php @@ -7,6 +7,7 @@ * 先到先得,领完或超时后自动关闭。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ diff --git a/app/Models/SysParam.php b/app/Models/SysParam.php index 467655d..8f6b8ca 100644 --- a/app/Models/SysParam.php +++ b/app/Models/SysParam.php @@ -5,8 +5,8 @@ * 对应原版 ASP 聊天室的 sysparam 配置表 * 管理员可在后台修改等级经验阈值等系统参数 * - * @package App\Models * @author ChatRoom Laravel + * * @version 1.0.0 */ @@ -27,9 +27,8 @@ class Sysparam extends Model * 获取指定参数的值 * 带缓存,避免频繁查库 * - * @param string $alias 参数别名 - * @param string $default 默认值 - * @return string + * @param string $alias 参数别名 + * @param string $default 默认值 */ public static function getValue(string $alias, string $default = ''): string { @@ -56,7 +55,7 @@ class Sysparam extends Model /** * 根据经验值计算应该达到的等级 * - * @param int $expNum 当前经验值 + * @param int $expNum 当前经验值 * @return int 对应的等级 */ public static function calculateLevel(int $expNum): int @@ -81,7 +80,7 @@ class Sysparam extends Model /** * 清除指定参数的缓存 * - * @param string $alias 参数别名 + * @param string $alias 参数别名 */ public static function clearCache(string $alias): void { diff --git a/app/Models/User.php b/app/Models/User.php index d17f6df..093bc80 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -90,18 +90,61 @@ class User extends Authenticatable * 头像文件名访问器 * * 原 ASP 系统的头像文件名存储在 usersf 字段中(如 "75.gif"), - * 但项目中各处通过 $user->headface 来引用头像。 - * 此 accessor 将 headface 属性映射到 usersf 字段,保持代码一致性。 + * 同时也支持用户自定义上传的头像,保存在 Laravel Storage 的 public 磁盘下。 + * 此 accessor 将 headface 属性映射到 usersf 字段,如果包含 storage/ 则当作独立路径,自动转换旧版后缀小写。 */ protected function headface(): Attribute { return Attribute::make( - // 自动将后缀转小写,兼容数据库中的 .GIF 大写存量 - get: fn () => strtolower($this->usersf ?: '1.gif'), - set: fn (string $value) => ['usersf' => strtolower($value)], + get: function () { + $val = $this->usersf ?: '1.gif'; + if (str_starts_with($val, 'storage/')) { + return $val; + } + + // 仅对非 storage 下的旧头像做小写处理,兼容旧库数据 + return strtolower($val); + }, + set: function ($value) { + if (str_starts_with($value, 'storage/')) { + return tap($value, fn () => $this->attributes['usersf'] = $value); + } + + return tap(strtolower($value), fn () => $this->attributes['usersf'] = strtolower($value)); + } ); } + /** + * 获取带前缀的完整头像 URL + * 避免前端多处硬编码 '/images/headface/' + */ + protected function headfaceUrl(): Attribute + { + return Attribute::make( + get: function () { + $hf = $this->headface; + if (str_starts_with((string) $hf, 'storage/')) { + return '/'.$hf; + } + + return '/images/headface/'.$hf; + } + ); + } + + /** + * 如果当前头像是自定义上传的图片,则从本地存储中删除此文件 + */ + public function deleteCustomAvatar(): void + { + $hf = (string) $this->usersf; + if (str_starts_with($hf, 'storage/')) { + $path = substr($hf, 8); // 去除 'storage/' 前缀 + \Illuminate\Support\Facades\Storage::disk('public')->delete($path); + } + } + /** * 关联:用户所属的 VIP 会员等级 */ diff --git a/app/Models/UserCurrencyLog.php b/app/Models/UserCurrencyLog.php index fd94efa..8bcedf1 100644 --- a/app/Models/UserCurrencyLog.php +++ b/app/Models/UserCurrencyLog.php @@ -6,6 +6,7 @@ * 只读写,不允许 update(流水记录不可更改) * * @author ChatRoom Laravel + * * @version 1.0.0 */ @@ -40,10 +41,10 @@ class UserCurrencyLog extends Model * 字段类型转换 */ protected $casts = [ - 'amount' => 'integer', - 'balance_after'=> 'integer', - 'room_id' => 'integer', - 'created_at' => 'datetime', + 'amount' => 'integer', + 'balance_after' => 'integer', + 'room_id' => 'integer', + 'created_at' => 'datetime', ]; // ─── 关联 ───────────────────────────────────────────────── diff --git a/app/Services/ChatStateService.php b/app/Services/ChatStateService.php index 16f11b6..7f73239 100644 --- a/app/Services/ChatStateService.php +++ b/app/Services/ChatStateService.php @@ -131,7 +131,7 @@ class ChatStateService { $usernames = []; foreach ($this->getAllActiveRoomIds() as $roomId) { - $key = "room:{$roomId}:users"; + $key = "room:{$roomId}:users"; $users = Redis::hkeys($key); // 只取 key(用户名),不取 value foreach ($users as $username) { $usernames[] = $username; diff --git a/app/Services/UserCurrencyService.php b/app/Services/UserCurrencyService.php index 12841e2..06a8869 100644 --- a/app/Services/UserCurrencyService.php +++ b/app/Services/UserCurrencyService.php @@ -7,6 +7,7 @@ * 本服务负责:原子性更新用户属性、写入流水记录、提供统计与排行数据。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ @@ -25,8 +26,8 @@ class UserCurrencyService * 以后新增货币类型,在此加一行即可。 */ private const FIELD_MAP = [ - 'exp' => 'exp_num', - 'gold' => 'jjb', + 'exp' => 'exp_num', + 'gold' => 'jjb', 'charm' => 'meili', ]; @@ -34,20 +35,20 @@ class UserCurrencyService * 统一变更用户货币属性并写入流水记录。 * 使用数据库事务保证原子性:用户属性更新 + 流水写入同时成功或同时回滚。 * - * @param User $user 目标用户 - * @param string $currency 货币类型('exp' / 'gold' / 'charm') - * @param int $amount 变更量,正数增加,负数扣除 - * @param CurrencySource $source 来源活动枚举 - * @param string $remark 备注说明 - * @param int|null $roomId 所在房间 ID(可选) + * @param User $user 目标用户 + * @param string $currency 货币类型('exp' / 'gold' / 'charm') + * @param int $amount 变更量,正数增加,负数扣除 + * @param CurrencySource $source 来源活动枚举 + * @param string $remark 备注说明 + * @param int|null $roomId 所在房间 ID(可选) */ public function change( - User $user, - string $currency, - int $amount, + User $user, + string $currency, + int $amount, CurrencySource $source, - string $remark = '', - ?int $roomId = null, + string $remark = '', + ?int $roomId = null, ): void { if ($amount === 0) { return; // 变更量为 0 不写记录 @@ -72,14 +73,14 @@ class UserCurrencyService // 写入流水记录(快照当前用户名,排行 JOIN 时再取最新名) UserCurrencyLog::create([ - 'user_id' => $user->id, - 'username' => $user->username, - 'currency' => $currency, - 'amount' => $amount, - 'balance_after'=> $balanceAfter, - 'source' => $source->value, - 'remark' => $remark, - 'room_id' => $roomId, + 'user_id' => $user->id, + 'username' => $user->username, + 'currency' => $currency, + 'amount' => $amount, + 'balance_after' => $balanceAfter, + 'source' => $source->value, + 'remark' => $remark, + 'room_id' => $roomId, ]); }); } @@ -88,14 +89,12 @@ class UserCurrencyService * 批量变更多个用户的货币属性(适用于自动存点:一次操作多人)。 * 每位用户仍独立走事务,单人失败不影响其他人。 * - * @param array $items [['user' => User, 'changes' => ['exp'=>1,'gold'=>2]], ...] - * @param CurrencySource $source - * @param int|null $roomId + * @param array $items [['user' => User, 'changes' => ['exp'=>1,'gold'=>2]], ...] */ public function batchChange(array $items, CurrencySource $source, ?int $roomId = null): void { foreach ($items as $item) { - $user = $item['user']; + $user = $item['user']; $changes = $item['changes'] ?? []; foreach ($changes as $currency => $amount) { $this->change($user, $currency, (int) $amount, $source, '', $roomId); @@ -127,8 +126,8 @@ class UserCurrencyService * 只统计正向变更(amount > 0),不因消耗而扣分。 * * @param string $currency 'exp' | 'gold' | 'charm' - * @param int $limit 返回条数 - * @param string|null $date 日期,默认今日 + * @param int $limit 返回条数 + * @param string|null $date 日期,默认今日 */ public function todayLeaderboard(string $currency, int $limit = 20, ?string $date = null): Collection { @@ -149,12 +148,12 @@ class UserCurrencyService ->find($row->user_id); return (object) [ - 'user_id' => $row->user_id, + 'user_id' => $row->user_id, 'username' => $user?->username ?? '未知用户', - 'level' => $user?->user_level ?? 0, - 'sex' => $user?->sex ?? 1, + 'level' => $user?->user_level ?? 0, + 'sex' => $user?->sex ?? 1, 'headface' => $user?->headface ?? '1.gif', - 'total' => $row->total, + 'total' => $row->total, ]; }); } @@ -162,9 +161,9 @@ class UserCurrencyService /** * 用户个人流水明细(用户查询自己的日志)。 * - * @param int $userId 用户 ID + * @param int $userId 用户 ID * @param string|null $currency 为 null 时返回所有货币类型 - * @param int $days 查询最近多少天 + * @param int $days 查询最近多少天 */ public function userLogs(int $userId, ?string $currency = null, int $days = 7): Collection { @@ -183,8 +182,8 @@ class UserCurrencyService public static function currencyLabel(string $currency): string { return match ($currency) { - 'exp' => '经验', - 'gold' => '金币', + 'exp' => '经验', + 'gold' => '金币', 'charm' => '魅力', default => $currency, }; diff --git a/composer.json b/composer.json index 8f9ea0e..a50b14c 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "require": { "php": "^8.2", "geoip2/geoip2": "^3.3", + "intervention/image": "^3.11", "laravel/framework": "^12.0", "laravel/horizon": "^5.45", "laravel/reverb": "^1.8", diff --git a/composer.lock b/composer.lock index ac3d21d..ad783b7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e501ac28571f87b0f192898f912648f5", + "content-hash": "56338775768722c90ec723eb5b939be1", "packages": [ { "name": "brick/math", diff --git a/database/migrations/2026_02_26_092923_create_sysparam_table.php b/database/migrations/2026_02_26_092923_create_sysparam_table.php index 7433369..b85fc62 100644 --- a/database/migrations/2026_02_26_092923_create_sysparam_table.php +++ b/database/migrations/2026_02_26_092923_create_sysparam_table.php @@ -48,21 +48,21 @@ return new class extends Migration ['alias' => 'level_warn', 'body' => '5', 'guidetxt' => '警告所需等级', 'created_at' => $now, 'updated_at' => $now], ['alias' => 'level_mute', 'body' => '50', 'guidetxt' => '禁言所需等级', 'created_at' => $now, 'updated_at' => $now], ['alias' => 'level_kick', 'body' => '60', 'guidetxt' => '踢人所需等级', 'created_at' => $now, 'updated_at' => $now], - ['alias' => 'level_announcement','body' => '60', 'guidetxt' => '设置公告所需等级', 'created_at' => $now, 'updated_at' => $now], + ['alias' => 'level_announcement', 'body' => '60', 'guidetxt' => '设置公告所需等级', 'created_at' => $now, 'updated_at' => $now], ['alias' => 'level_ban', 'body' => '80', 'guidetxt' => '封号所需等级', 'created_at' => $now, 'updated_at' => $now], ['alias' => 'level_banip', 'body' => '90', 'guidetxt' => '封IP所需等级', 'created_at' => $now, 'updated_at' => $now], ['alias' => 'level_freeze', 'body' => '14', 'guidetxt' => '冻结账号所需等级', 'created_at' => $now, 'updated_at' => $now], // ── 随机事件 ────────────────────────────────────────────── - ['alias' => 'auto_event_chance','body' => '10', 'guidetxt' => '随机事件触发概率(百分比,1-100)', 'created_at' => $now, 'updated_at' => $now], + ['alias' => 'auto_event_chance', 'body' => '10', 'guidetxt' => '随机事件触发概率(百分比,1-100)', 'created_at' => $now, 'updated_at' => $now], // ── 魅力系统 ────────────────────────────────────────────── ['alias' => 'charm_cross_sex', 'body' => '2', 'guidetxt' => '异性聊天每条消息增加的魅力值', 'created_at' => $now, 'updated_at' => $now], ['alias' => 'charm_same_sex', 'body' => '1', 'guidetxt' => '同性聊天每条消息增加的魅力值', 'created_at' => $now, 'updated_at' => $now], - ['alias' => 'charm_hourly_limit','body' => '20', 'guidetxt' => '每小时通过聊天获取的魅力值上限(防刷屏)', 'created_at' => $now, 'updated_at' => $now], + ['alias' => 'charm_hourly_limit', 'body' => '20', 'guidetxt' => '每小时通过聊天获取的魅力值上限(防刷屏)', 'created_at' => $now, 'updated_at' => $now], // ── 排行榜 ──────────────────────────────────────────────── - ['alias' => 'leaderboard_limit','body' => '20', 'guidetxt' => '🏆 排行榜每榜显示人数', 'created_at' => $now, 'updated_at' => $now], + ['alias' => 'leaderboard_limit', 'body' => '20', 'guidetxt' => '🏆 排行榜每榜显示人数', 'created_at' => $now, 'updated_at' => $now], ]); } diff --git a/database/migrations/2026_02_27_074855_create_shop_tables.php b/database/migrations/2026_02_27_074855_create_shop_tables.php index 5cfdf0d..948f6db 100644 --- a/database/migrations/2026_02_27_074855_create_shop_tables.php +++ b/database/migrations/2026_02_27_074855_create_shop_tables.php @@ -7,6 +7,7 @@ * username_blacklist — 用户改名后的旧名称保留黑名单 * * @author ChatRoom Laravel + * * @version 1.0.0 */ diff --git a/database/migrations/2026_02_28_123839_create_user_currency_logs_table.php b/database/migrations/2026_02_28_123839_create_user_currency_logs_table.php index 8e97a04..74849ef 100644 --- a/database/migrations/2026_02_28_123839_create_user_currency_logs_table.php +++ b/database/migrations/2026_02_28_123839_create_user_currency_logs_table.php @@ -5,6 +5,7 @@ * 记录所有用户经验/金币/魅力的变动来源与金额,支持今日排行与活动统计 * * @author ChatRoom Laravel + * * @version 1.0.0 */ diff --git a/database/migrations/2026_03_01_140713_make_reserved_until_nullable_in_username_blacklist.php b/database/migrations/2026_03_01_140713_make_reserved_until_nullable_in_username_blacklist.php index 553924a..4717b71 100644 --- a/database/migrations/2026_03_01_140713_make_reserved_until_nullable_in_username_blacklist.php +++ b/database/migrations/2026_03_01_140713_make_reserved_until_nullable_in_username_blacklist.php @@ -30,7 +30,7 @@ return new class extends Migration public function down(): void { // 先把现有 NULL 行补一个兜底值,再改回 NOT NULL - DB::statement("UPDATE `username_blacklist` SET `reserved_until` = NOW() + INTERVAL 365 DAY WHERE `reserved_until` IS NULL"); - DB::statement("ALTER TABLE `username_blacklist` MODIFY `reserved_until` TIMESTAMP NOT NULL"); + DB::statement('UPDATE `username_blacklist` SET `reserved_until` = NOW() + INTERVAL 365 DAY WHERE `reserved_until` IS NULL'); + DB::statement('ALTER TABLE `username_blacklist` MODIFY `reserved_until` TIMESTAMP NOT NULL'); } }; diff --git a/database/migrations/2026_03_01_181922_add_divorce_cooldown_to_marriage_configs.php b/database/migrations/2026_03_01_181922_add_divorce_cooldown_to_marriage_configs.php index 61667b6..e5984a9 100644 --- a/database/migrations/2026_03_01_181922_add_divorce_cooldown_to_marriage_configs.php +++ b/database/migrations/2026_03_01_181922_add_divorce_cooldown_to_marriage_configs.php @@ -12,42 +12,42 @@ return new class extends Migration { DB::table('marriage_configs')->insertOrIgnore([ [ - 'group' => '时间规则', - 'key' => 'divorce_mutual_cooldown', - 'value' => 70, - 'label' => '协议离婚冷静期(天)', + 'group' => '时间规则', + 'key' => 'divorce_mutual_cooldown', + 'value' => 70, + 'label' => '协议离婚冷静期(天)', 'description' => '协议离婚成功后,多少天内不能再次结婚 (支持设置为 0)', - 'min' => 0, - 'max' => 365, - 'created_at' => now(), - 'updated_at' => now(), + 'min' => 0, + 'max' => 365, + 'created_at' => now(), + 'updated_at' => now(), ], [ - 'group' => '时间规则', - 'key' => 'divorce_auto_cooldown', - 'value' => 70, - 'label' => '系统强制离婚冷静期(天)', + 'group' => '时间规则', + 'key' => 'divorce_auto_cooldown', + 'value' => 70, + 'label' => '系统强制离婚冷静期(天)', 'description' => '单方面申请协议离婚但对方不管,导致超时后系统强制离婚的冷静期', - 'min' => 0, - 'max' => 365, - 'created_at' => now(), - 'updated_at' => now(), + 'min' => 0, + 'max' => 365, + 'created_at' => now(), + 'updated_at' => now(), ], ]); - + // 由于离婚有 mutual, auto, forced 等,我们同时添加 forced 参数的插入以防遗漏 DB::table('marriage_configs')->insertOrIgnore([ [ - 'group' => '时间规则', - 'key' => 'divorce_forced_cooldown', - 'value' => 70, - 'label' => '单方强制离婚冷静期(天)', + 'group' => '时间规则', + 'key' => 'divorce_forced_cooldown', + 'value' => 70, + 'label' => '单方强制离婚冷静期(天)', 'description' => '通过单方面强制解除契约功能导致的离婚冷静期', - 'min' => 0, - 'max' => 365, - 'created_at' => now(), - 'updated_at' => now(), - ] + 'min' => 0, + 'max' => 365, + 'created_at' => now(), + 'updated_at' => now(), + ], ]); } diff --git a/database/migrations/2026_03_01_212100_create_red_packet_envelopes_table.php b/database/migrations/2026_03_01_212100_create_red_packet_envelopes_table.php index a7027e8..a76ffbd 100644 --- a/database/migrations/2026_03_01_212100_create_red_packet_envelopes_table.php +++ b/database/migrations/2026_03_01_212100_create_red_packet_envelopes_table.php @@ -7,6 +7,7 @@ * red_packet_claims:红包领取记录(先到先得,每人只能领一次) * * @author ChatRoom Laravel + * * @version 1.0.0 */ diff --git a/database/migrations/2026_03_01_213755_add_type_to_red_packet_envelopes_table.php b/database/migrations/2026_03_01_213755_add_type_to_red_packet_envelopes_table.php index 8a9cac3..04377ff 100644 --- a/database/migrations/2026_03_01_213755_add_type_to_red_packet_envelopes_table.php +++ b/database/migrations/2026_03_01_213755_add_type_to_red_packet_envelopes_table.php @@ -7,6 +7,7 @@ * 默认 gold,兼容已有记录。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ diff --git a/database/migrations/2026_03_03_175451_create_mystery_boxes_table.php b/database/migrations/2026_03_03_175451_create_mystery_boxes_table.php index 4fa2843..937ad2d 100644 --- a/database/migrations/2026_03_03_175451_create_mystery_boxes_table.php +++ b/database/migrations/2026_03_03_175451_create_mystery_boxes_table.php @@ -6,6 +6,7 @@ * 记录每次系统/管理员投放的神秘箱信息,包含类型、暗号、奖惩范围及领取状态。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ diff --git a/database/migrations/2026_03_03_175452_create_mystery_box_claims_table.php b/database/migrations/2026_03_03_175452_create_mystery_box_claims_table.php index 0e5eeda..666635c 100644 --- a/database/migrations/2026_03_03_175452_create_mystery_box_claims_table.php +++ b/database/migrations/2026_03_03_175452_create_mystery_box_claims_table.php @@ -6,6 +6,7 @@ * 记录每个箱子被哪位用户在何时用什么暗号领取,以及实际奖励金额。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ diff --git a/database/migrations/2026_03_04_152932_create_lottery_issues_table.php b/database/migrations/2026_03_04_152932_create_lottery_issues_table.php index 36d7bfe..0834fd0 100644 --- a/database/migrations/2026_03_04_152932_create_lottery_issues_table.php +++ b/database/migrations/2026_03_04_152932_create_lottery_issues_table.php @@ -6,6 +6,7 @@ * 记录每期彩票的开奖状态、号码、奖池金额、派奖结果。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ diff --git a/database/migrations/2026_03_04_152933_create_lottery_pool_logs_table.php b/database/migrations/2026_03_04_152933_create_lottery_pool_logs_table.php index 87821de..ae82c30 100644 --- a/database/migrations/2026_03_04_152933_create_lottery_pool_logs_table.php +++ b/database/migrations/2026_03_04_152933_create_lottery_pool_logs_table.php @@ -6,6 +6,7 @@ * 透明记录每期奖池的每笔变动(售票入池、派奖扣除、滚存、系统注入)。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ diff --git a/database/migrations/2026_03_04_152933_create_lottery_tickets_table.php b/database/migrations/2026_03_04_152933_create_lottery_tickets_table.php index f969e8d..74d5a87 100644 --- a/database/migrations/2026_03_04_152933_create_lottery_tickets_table.php +++ b/database/migrations/2026_03_04_152933_create_lottery_tickets_table.php @@ -6,6 +6,7 @@ * 记录每用户每注的选号、中奖等级、派奖金额。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ diff --git a/database/seeders/AutoFishingCardSeeder.php b/database/seeders/AutoFishingCardSeeder.php index d54a0e1..76aa551 100644 --- a/database/seeders/AutoFishingCardSeeder.php +++ b/database/seeders/AutoFishingCardSeeder.php @@ -55,15 +55,15 @@ class AutoFishingCardSeeder extends Seeder 'is_active' => true, ], [ - 'slug' => 'auto_fishing_72h', - 'name' => '自动钓鱼卡(72小时)', - 'icon' => '🎣', - 'description' => '激活后72小时内,钓鱼无需手动点击浮漂,系统自动收竿。钓鱼大神终极之选!', - 'price' => 15000, - 'type' => 'auto_fishing', + 'slug' => 'auto_fishing_72h', + 'name' => '自动钓鱼卡(72小时)', + 'icon' => '🎣', + 'description' => '激活后72小时内,钓鱼无需手动点击浮漂,系统自动收竿。钓鱼大神终极之选!', + 'price' => 15000, + 'type' => 'auto_fishing', 'duration_minutes' => 4320, - 'sort_order' => 204, - 'is_active' => true, + 'sort_order' => 204, + 'is_active' => true, ], ]; diff --git a/database/seeders/FishingEventSeeder.php b/database/seeders/FishingEventSeeder.php index 3ea7f04..92c814f 100644 --- a/database/seeders/FishingEventSeeder.php +++ b/database/seeders/FishingEventSeeder.php @@ -43,73 +43,73 @@ class FishingEventSeeder extends Seeder $events = [ [ - 'sort' => 1, - 'emoji' => '🦈', - 'name' => '大鲨鱼', - 'message' => '钓到一条大鲨鱼!获得经验30、金币50', - 'exp' => 30, - 'jjb' => 50, - 'weight' => 15, + 'sort' => 1, + 'emoji' => '🦈', + 'name' => '大鲨鱼', + 'message' => '钓到一条大鲨鱼!获得经验30、金币50', + 'exp' => 30, + 'jjb' => 50, + 'weight' => 15, 'is_active' => true, ], [ - 'sort' => 2, - 'emoji' => '🐟', - 'name' => '娃娃鱼', - 'message' => '钓到一条娃娃鱼,到集市卖得80个金币', - 'exp' => 0, - 'jjb' => 80, - 'weight' => 15, + 'sort' => 2, + 'emoji' => '🐟', + 'name' => '娃娃鱼', + 'message' => '钓到一条娃娃鱼,到集市卖得80个金币', + 'exp' => 0, + 'jjb' => 80, + 'weight' => 15, 'is_active' => true, ], [ - 'sort' => 3, - 'emoji' => '🐠', - 'name' => '大草鱼', - 'message' => '钓到一只大草鱼,吃下增加经验20、金币30', - 'exp' => 20, - 'jjb' => 30, - 'weight' => 20, + 'sort' => 3, + 'emoji' => '🐠', + 'name' => '大草鱼', + 'message' => '钓到一只大草鱼,吃下增加经验20、金币30', + 'exp' => 20, + 'jjb' => 30, + 'weight' => 20, 'is_active' => true, ], [ - 'sort' => 4, - 'emoji' => '🐡', - 'name' => '小鲤鱼', - 'message' => '钓到一条小鲤鱼,增加经验10、金币20', - 'exp' => 10, - 'jjb' => 20, - 'weight' => 20, + 'sort' => 4, + 'emoji' => '🐡', + 'name' => '小鲤鱼', + 'message' => '钓到一条小鲤鱼,增加经验10、金币20', + 'exp' => 10, + 'jjb' => 20, + 'weight' => 20, 'is_active' => true, ], [ - 'sort' => 5, - 'emoji' => '💧', - 'name' => '落水惨败', - 'message' => '鱼没钓到,摔到河里损失金币30', - 'exp' => 0, - 'jjb' => -30, - 'weight' => 15, + 'sort' => 5, + 'emoji' => '💧', + 'name' => '落水惨败', + 'message' => '鱼没钓到,摔到河里损失金币30', + 'exp' => 0, + 'jjb' => -30, + 'weight' => 15, 'is_active' => true, ], [ - 'sort' => 6, - 'emoji' => '👊', - 'name' => '被抓殴打', - 'message' => '偷钓鱼塘被主人发现,一阵殴打!金币减少10', - 'exp' => 0, - 'jjb' => -10, - 'weight' => 10, + 'sort' => 6, + 'emoji' => '👊', + 'name' => '被抓殴打', + 'message' => '偷钓鱼塘被主人发现,一阵殴打!金币减少10', + 'exp' => 0, + 'jjb' => -10, + 'weight' => 10, 'is_active' => true, ], [ - 'sort' => 7, - 'emoji' => '🎉', - 'name' => '超级大奖', - 'message' => '运气爆棚!钓到大鲨鱼、大草鱼、小鲤鱼各一条!经验+50,金币+200!', - 'exp' => 50, - 'jjb' => 200, - 'weight' => 5, + 'sort' => 7, + 'emoji' => '🎉', + 'name' => '超级大奖', + 'message' => '运气爆棚!钓到大鲨鱼、大草鱼、小鲤鱼各一条!经验+50,金币+200!', + 'exp' => 50, + 'jjb' => 200, + 'weight' => 5, 'is_active' => true, ], ]; diff --git a/database/seeders/GameConfigSeeder.php b/database/seeders/GameConfigSeeder.php index 70505ff..8008103 100644 --- a/database/seeders/GameConfigSeeder.php +++ b/database/seeders/GameConfigSeeder.php @@ -70,16 +70,16 @@ class GameConfigSeeder extends Seeder 'description' => '管理员随时投放或系统定时自动投放神秘箱,最快发送暗号的用户开箱获得奖励。', 'enabled' => false, 'params' => [ - 'auto_drop_enabled' => false, // 是否自动定时投放 - 'auto_interval_hours' => 2, // 自动投放间隔(小时) + 'auto_drop_enabled' => false, // 是否自动定时投放 + 'auto_interval_hours' => 2, // 自动投放间隔(小时) 'claim_window_seconds' => 60, // 领取窗口(秒) - 'normal_reward_min' => 500, // 普通箱最低奖励 - 'normal_reward_max' => 2000, // 普通箱最高奖励 - 'rare_reward_min' => 5000, // 稀有箱最低奖励 - 'rare_reward_max' => 20000, // 稀有箱最高奖励 - 'trap_penalty_min' => 200, // 黑化箱最低惩罚 - 'trap_penalty_max' => 1000, // 黑化箱最高惩罚 - 'trap_chance_percent' => 10, // 黑化箱触发概率(%) + 'normal_reward_min' => 500, // 普通箱最低奖励 + 'normal_reward_max' => 2000, // 普通箱最高奖励 + 'rare_reward_min' => 5000, // 稀有箱最低奖励 + 'rare_reward_max' => 20000, // 稀有箱最高奖励 + 'trap_penalty_min' => 200, // 黑化箱最低惩罚 + 'trap_penalty_max' => 1000, // 黑化箱最高惩罚 + 'trap_chance_percent' => 10, // 黑化箱触发概率(%) ], ], @@ -121,13 +121,13 @@ class GameConfigSeeder extends Seeder // ─── 钓鱼小游戏 ────────────────────────────────────────────── [ - 'game_key' => 'fishing', - 'name' => '钓鱼小游戏', - 'icon' => '🎣', + 'game_key' => 'fishing', + 'name' => '钓鱼小游戏', + 'icon' => '🎣', 'description' => '消耗金币抛竿,等待浮漂下沉后点击收竿,随机获得奖励或惩罚。持有自动钓鱼卡可自动循环。', - 'enabled' => false, - 'params' => [ - 'fishing_cost' => 5, // 每次抛竿消耗金币 + 'enabled' => false, + 'params' => [ + 'fishing_cost' => 5, // 每次抛竿消耗金币 'fishing_wait_min' => 8, // 浮漂等待最短秒数 'fishing_wait_max' => 15, // 浮漂等待最长秒数 'fishing_cooldown' => 300, // 收竿后冷却秒数 @@ -136,36 +136,36 @@ class GameConfigSeeder extends Seeder // ─── 双色球彩票 ────────────────────────────────────────────── [ - 'game_key' => 'lottery', - 'name' => '双色球彩票', - 'icon' => '🎟️', + 'game_key' => 'lottery', + 'name' => '双色球彩票', + 'icon' => '🎟️', 'description' => '每日一期,选3红球(1-12)+1蓝球(1-6),按奖池比例派奖,无一等奖滚存累积。', - 'enabled' => false, - 'params' => [ + 'enabled' => false, + 'params' => [ // ── 开奖时间 ── - 'draw_hour' => 20, // 每天几点开奖(24小时制) - 'draw_minute' => 0, // 几分开奖 - 'stop_sell_minutes' => 2, // 开奖前几分钟停止购票 + 'draw_hour' => 20, // 每天几点开奖(24小时制) + 'draw_minute' => 0, // 几分开奖 + 'stop_sell_minutes' => 2, // 开奖前几分钟停止购票 // ── 购票限制 ── - 'ticket_price' => 100, // 每注金币 - 'max_tickets_per_user' => 50, // 每期单人最多购票注数 - 'max_tickets_per_buy' => 10, // 单次最多购买注数 + 'ticket_price' => 100, // 每注金币 + 'max_tickets_per_user' => 50, // 每期单人最多购票注数 + 'max_tickets_per_buy' => 10, // 单次最多购买注数 // ── 奖池分配比例(%)── - 'pool_ratio' => 70, // 购票金额进奖池比例 - 'prize_1st_ratio' => 60, // 一等奖占奖池% - 'prize_2nd_ratio' => 20, // 二等奖占奖池% - 'prize_3rd_ratio' => 10, // 三等奖占奖池% - 'carry_ratio' => 10, // 强制滚存比例% + 'pool_ratio' => 70, // 购票金额进奖池比例 + 'prize_1st_ratio' => 60, // 一等奖占奖池% + 'prize_2nd_ratio' => 20, // 二等奖占奖池% + 'prize_3rd_ratio' => 10, // 三等奖占奖池% + 'carry_ratio' => 10, // 强制滚存比例% // ── 固定小奖 ── - 'prize_4th_fixed' => 150, // 四等奖固定金额/注 - 'prize_5th_fixed' => 50, // 五等奖固定金额/注 + 'prize_4th_fixed' => 150, // 四等奖固定金额/注 + 'prize_5th_fixed' => 50, // 五等奖固定金额/注 // ── 超级期 ── 'super_issue_threshold' => 3, // 连续几期无一等奖触发超级期 - 'super_issue_inject' => 20000, // 超级期系统注入金额上限 + 'super_issue_inject' => 20000, // 超级期系统注入金额上限 ], ], ]; diff --git a/database/seeders/ShopItemSeeder.php b/database/seeders/ShopItemSeeder.php index 945f953..5e668c9 100644 --- a/database/seeders/ShopItemSeeder.php +++ b/database/seeders/ShopItemSeeder.php @@ -3,8 +3,6 @@ /** * 文件功能:商店初始商品数据填充器 * 初始化9种商品:4种单次特效卡 + 4种周卡 + 改名卡 - * - * @package Database\Seeders */ namespace Database\Seeders; diff --git a/resources/views/admin/users/index.blade.php b/resources/views/admin/users/index.blade.php index b174328..2823cbb 100644 --- a/resources/views/admin/users/index.blade.php +++ b/resources/views/admin/users/index.blade.php @@ -81,7 +81,7 @@ {{ $user->id }}
- {{ $user->username }} @if ($user->isVip()) diff --git a/resources/views/chat/partials/friend-panel.blade.php b/resources/views/chat/partials/friend-panel.blade.php index 049d850..4f3650f 100644 --- a/resources/views/chat/partials/friend-panel.blade.php +++ b/resources/views/chat/partials/friend-panel.blade.php @@ -415,7 +415,8 @@ const avatar = document.createElement('img'); avatar.className = 'fp-avatar'; - avatar.src = '/images/headface/' + (f.headface || '1.gif'); + let hf = f.headface || '1.gif'; + avatar.src = hf.startsWith('storage/') ? '/' + hf : '/images/headface/' + hf; avatar.alt = f.username; const name = document.createElement('span'); @@ -456,7 +457,8 @@ const avatar = document.createElement('img'); avatar.className = 'fp-avatar'; - avatar.src = '/images/headface/' + (p.headface || '1.gif'); + let hf = p.headface || '1.gif'; + avatar.src = hf.startsWith('storage/') ? '/' + hf : '/images/headface/' + hf; avatar.alt = p.username; const name = document.createElement('span'); diff --git a/resources/views/chat/partials/layout/toolbar.blade.php b/resources/views/chat/partials/layout/toolbar.blade.php index ba72cad..6e304ca 100644 --- a/resources/views/chat/partials/layout/toolbar.blade.php +++ b/resources/views/chat/partials/layout/toolbar.blade.php @@ -59,14 +59,22 @@ {{-- 预览区 --}}
+ style="padding:10px 16px; background:#f0f6ff; border-bottom:1px solid #ddd; display:flex; align-items:center; gap:12px; flex-wrap:wrap;"> 当前选中: - + {{ $user->usersf ?: '未设置' }} + border-radius:3px; font-size:12px; cursor:pointer;">确定更换系统头像 +
+
+ 自定义头像上传(112x112): + + +
{{-- 头像网格 --}} @@ -250,6 +258,74 @@ document.getElementById('avatar-save-btn').dataset.file = file; } + /** + * 处理本地头像上传 + */ + async function handleAvatarUpload(input) { + if (!input.files || !input.files[0]) return; + + const file = input.files[0]; + + // 简单的前端校验 + if (file.size > 2 * 1024 * 1024) { + window.chatDialog.alert('图片大小不可超过 2MB', '上传失败', '#cc4444'); + input.value = ''; + return; + } + + const btn = document.getElementById('avatar-upload-btn'); + btn.disabled = true; + btn.textContent = '上传中...'; + + const formData = new FormData(); + formData.append('file', file); + + try { + const res = await fetch('/headface/upload', { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, + 'Accept': 'application/json' + }, + body: formData + }); + const data = await res.json(); + + if (res.ok && data.status === 'success') { + window.chatDialog.alert('自定义头像上传成功!', '提示', '#16a34a'); + + // 更新预览图和显示名称 + const previewImg = document.getElementById('avatar-preview'); + const relativeUrl = '/' + data.headface; + previewImg.src = relativeUrl; + document.getElementById('avatar-selected-name').textContent = data.headface; + + // 同步在线列表自己 + const myName = window.chatContext.username; + if (typeof onlineUsers !== 'undefined' && onlineUsers[myName]) { + onlineUsers[myName].headface = data.headface; + } + if (typeof renderUserList === 'function') { + renderUserList(); + } + + // 清除系统头像选中状态 + document.querySelectorAll('.avatar-option.selected').forEach(el => el.classList.remove('selected')); + document.getElementById('avatar-save-btn').disabled = true; + + closeAvatarPicker(); + } else { + window.chatDialog.alert(data.message || '上传失败', '操作失败', '#cc4444'); + } + } catch (e) { + window.chatDialog.alert('网络错误,上传失败', '网络异常', '#cc4444'); + } + + btn.disabled = false; + btn.textContent = '选择本地图片上传'; + input.value = ''; // 清空 file input,允许重复选中同一文件 + } + /** * 保存选中的头像(调用 API 更新,成功后刷新用户列表) */ diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index 52ff9da..bebd795 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -205,6 +205,7 @@ item.dataset.username = username; const headface = (user.headface || '1.gif').toLowerCase(); + const headImgSrc = headface.startsWith('storage/') ? '/' + headface : '/images/headface/' + headface; // 徽章优先级:职务图标 > 管理员 > VIP let badges = ''; @@ -224,7 +225,7 @@ // 女生名字使用玫粉色 const nameColor = (user.sex == 2) ? 'color:#e91e8c;' : ''; item.innerHTML = ` - + ${username}${badges} `; @@ -369,7 +370,7 @@ const buggleUsers = ['钓鱼播报', '星海小博士', '送花播报', '系统传音', '系统公告']; const senderInfo = onlineUsers[msg.from_user]; const senderHead = ((senderInfo && senderInfo.headface) || '1.gif').toLowerCase(); - let headImgSrc = `/images/headface/${senderHead}`; + let headImgSrc = senderHead.startsWith('storage/') ? '/' + senderHead : `/images/headface/${senderHead}`; if (msg.from_user === 'AI小班长') { headImgSrc = '/images/ai_bot.png'; } else if (buggleUsers.includes(msg.from_user)) { diff --git a/resources/views/chat/partials/user-actions.blade.php b/resources/views/chat/partials/user-actions.blade.php index db359ae..6cf05a7 100644 --- a/resources/views/chat/partials/user-actions.blade.php +++ b/resources/views/chat/partials/user-actions.blade.php @@ -597,7 +597,7 @@