增加每日游戏三杰榜

This commit is contained in:
pllx
2026-05-09 10:23:38 +08:00
parent 41522393de
commit 1b062f67ea
5 changed files with 384 additions and 3 deletions
+3
View File
@@ -26,6 +26,7 @@ use App\Models\User;
use App\Services\AppointmentService;
use App\Services\ChatStateService;
use App\Services\ChatUserPresenceService;
use App\Services\DailyGameProfitLeaderboardService;
use App\Services\MessageFilterService;
use App\Services\PositionPermissionService;
use App\Services\RideService;
@@ -63,6 +64,7 @@ class ChatController extends Controller
private readonly VipService $vipService,
private readonly \App\Services\ShopService $shopService,
private readonly UserCurrencyService $currencyService,
private readonly DailyGameProfitLeaderboardService $dailyGameProfitLeaderboardService,
private readonly AppointmentService $appointmentService,
private readonly RoomBroadcastService $broadcast,
private readonly PositionPermissionService $positionPermissionService,
@@ -372,6 +374,7 @@ class ChatController extends Controller
'pendingDivorce' => $pendingDivorceData,
'roomPermissionMap' => $roomPermissionMap,
'hasRoomManagementPermission' => in_array(true, $roomPermissionMap, true),
'dailyGameProfitLeaders' => $this->dailyGameProfitLeaderboardService->topThree(),
'dailyStatusCatalog' => ChatDailyStatusCatalog::groupedOptions(),
'activeDailyStatus' => $this->chatUserPresenceService->currentDailyStatus($user),
]);
@@ -0,0 +1,103 @@
<?php
/**
* 文件功能:每日游戏净盈利前三榜读服务
*
* 聚合百家乐与赛马当天金币流水,给聊天室顶部悬浮榜提供轻量数据。
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Services;
use App\Enums\CurrencySource;
use App\Models\UserCurrencyLog;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
/**
* 类功能:查询百家乐与赛马每日净盈利前三用户。
*/
class DailyGameProfitLeaderboardService
{
/**
* 每日榜单固定称号。
*/
private const TITLES = [
1 => '金库爆破王',
2 => '马桌双修财神',
3 => '金币收割机',
];
/**
* 参与净盈利统计的游戏流水来源。
*/
private const GAME_PROFIT_SOURCES = [
CurrencySource::BACCARAT_BET,
CurrencySource::BACCARAT_WIN,
CurrencySource::HORSE_BET,
CurrencySource::HORSE_WIN,
];
/**
* 获取指定日期的游戏净盈利前三榜。
*
* @return Collection<int, object{rank:int,title:string,user_id:int,username:string,headface_url:string,net_profit:int}>
*/
public function topThree(?string $date = null): Collection
{
$statsDate = CarbonImmutable::parse($date ?? today()->toDateString())->startOfDay();
$cacheKey = 'daily_game_profit_leaderboard:v2:'.$statsDate->toDateString();
return Cache::remember($cacheKey, 300, function () use ($statsDate) {
$rangeStart = $statsDate;
$rangeEnd = $statsDate->addDay();
return UserCurrencyLog::query()
->join('users', 'users.id', '=', 'user_currency_logs.user_id')
->where('user_currency_logs.currency', 'gold')
->whereIn('user_currency_logs.source', array_map(
fn (CurrencySource $source): string => $source->value,
self::GAME_PROFIT_SOURCES
))
->where('user_currency_logs.created_at', '>=', $rangeStart)
->where('user_currency_logs.created_at', '<', $rangeEnd)
->where('users.username', '!=', 'AI小班长')
->groupBy('user_currency_logs.user_id', 'users.username', 'users.usersf')
->havingRaw('SUM(user_currency_logs.amount) > 0')
->orderByRaw('SUM(user_currency_logs.amount) DESC')
->orderBy('user_currency_logs.user_id')
->limit(3)
->selectRaw('user_currency_logs.user_id, users.username, users.usersf, SUM(user_currency_logs.amount) as net_profit')
->get()
->values()
->map(function (object $row, int $index): object {
$rank = $index + 1;
return (object) [
'rank' => $rank,
'title' => self::TITLES[$rank],
'user_id' => (int) $row->user_id,
'username' => (string) $row->username,
'headface_url' => $this->resolveHeadfaceUrl((string) ($row->usersf ?: '1.gif')),
'net_profit' => (int) $row->net_profit,
];
});
});
}
/**
* 解析榜单头像地址。
*/
private function resolveHeadfaceUrl(string $headface): string
{
if (str_starts_with($headface, 'storage/')) {
return '/'.$headface;
}
return '/images/headface/'.strtolower($headface);
}
}
+186
View File
@@ -170,6 +170,7 @@ a:hover {
.room-title-bar {
background: var(--bg-header);
color: var(--text-white);
position: relative;
text-align: center;
padding: 6px 10px;
font-size: 12px;
@@ -178,6 +179,10 @@ a:hover {
border-bottom: 1px solid var(--border-blue);
}
.room-title-main {
text-align: center;
}
.room-title-bar .room-name {
font-weight: bold;
font-size: 14px;
@@ -188,6 +193,145 @@ a:hover {
font-size: 11px;
}
.daily-game-profit-board {
position: absolute;
top: 8px;
right: 34px;
width: 132px;
color: #fff7ed;
text-align: center;
font-size: 13px;
line-height: 1.28;
z-index: 2;
}
.daily-game-profit-board-title {
color: #fde68a;
font-weight: bold;
margin-bottom: 5px;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
}
.daily-game-profit-avatars {
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
}
.daily-game-profit-avatar-wrap {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 999px;
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.24);
}
.daily-game-profit-avatar {
width: 30px;
height: 30px;
border-radius: 999px;
object-fit: cover;
background: rgba(255, 255, 255, 0.18);
}
.daily-game-profit-avatar-rank-1 {
background: linear-gradient(135deg, #fde047, #f59e0b 48%, #fff7ad);
}
.daily-game-profit-avatar-rank-1::before {
content: "1";
position: absolute;
right: -2px;
bottom: -2px;
width: 14px;
height: 14px;
border-radius: 999px;
background: #f59e0b;
color: #fff7ed;
font-size: 9px;
font-weight: bold;
line-height: 14px;
text-align: center;
box-shadow: 0 0 0 1px rgba(255, 247, 237, 0.9);
}
.daily-game-profit-avatar-rank-2 {
background: linear-gradient(135deg, #f8fafc, #94a3b8 48%, #ffffff);
}
.daily-game-profit-avatar-rank-2::before {
content: "2";
position: absolute;
right: -2px;
bottom: -2px;
width: 14px;
height: 14px;
border-radius: 999px;
background: #64748b;
color: #ffffff;
font-size: 9px;
font-weight: bold;
line-height: 14px;
text-align: center;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.9);
}
.daily-game-profit-avatar-rank-3 {
background: linear-gradient(135deg, #fed7aa, #c2410c 48%, #ffedd5);
}
.daily-game-profit-avatar-rank-3::before {
content: "3";
position: absolute;
right: -2px;
bottom: -2px;
width: 14px;
height: 14px;
border-radius: 999px;
background: #c2410c;
color: #fff7ed;
font-size: 9px;
font-weight: bold;
line-height: 14px;
text-align: center;
box-shadow: 0 0 0 1px rgba(255, 247, 237, 0.9);
}
.daily-game-profit-avatar-wrap::after {
content: attr(data-username);
position: absolute;
left: 50%;
bottom: -24px;
transform: translateX(-50%);
max-width: 110px;
padding: 3px 7px;
border-radius: 999px;
background: rgba(15, 23, 42, 0.88);
color: #ffffff;
font-size: 11px;
line-height: 1.2;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.12s ease;
overflow: hidden;
text-overflow: ellipsis;
}
.daily-game-profit-avatar-wrap:hover::after {
opacity: 1;
}
.daily-game-profit-empty {
color: #dbeafe;
font-size: 11px;
opacity: 0.82;
}
/* 公告/祝福语滚动条(复刻原版 marquee) */
.room-announcement-bar {
background: linear-gradient(to right, #a8d8a8, #c8f0c8, #a8d8a8);
@@ -956,6 +1100,48 @@ a:hover {
overflow-y: auto !important;
}
.daily-game-profit-board {
top: 5px;
right: 6px;
width: 102px;
font-size: 10px;
}
.daily-game-profit-empty {
display: none;
}
.daily-game-profit-board-title {
margin-bottom: 2px;
}
.daily-game-profit-avatars {
gap: 4px;
}
.daily-game-profit-avatar-wrap {
width: 25px;
height: 25px;
}
.daily-game-profit-avatar {
width: 22px;
height: 22px;
}
.daily-game-profit-avatar-rank-1::before,
.daily-game-profit-avatar-rank-2::before,
.daily-game-profit-avatar-rank-3::before {
width: 11px;
height: 11px;
font-size: 8px;
line-height: 11px;
}
.daily-game-profit-avatar-wrap::after {
display: none;
}
/* ── 手机端隐藏房间介绍 ── */
.room-desc {
display: none !important;
@@ -7,9 +7,28 @@
{{-- 顶部标题栏(第一行:房间名称 + 介绍) --}}
<div class="room-title-bar">
<div class="room-name">{{ $room->name }}<span style="font-size:12px; font-weight:normal;">在线<span
id="online-count">0</span></span></div>
<div class="room-desc" id="room-title-display">{{ $room->description ?? '欢迎光临本聊天室!畅所欲言,文明聊天。' }}
<div class="room-title-main">
<div class="room-name">{{ $room->name }}<span style="font-size:12px; font-weight:normal;">在线<span
id="online-count">0</span></span></div>
<div class="room-desc" id="room-title-display">{{ $room->description ?? '欢迎光临本聊天室!畅所欲言,文明聊天。' }}
</div>
</div>
<div class="daily-game-profit-board" aria-label="今日游戏三杰">
<div class="daily-game-profit-board-title">今日游戏三杰</div>
@if (($dailyGameProfitLeaders ?? collect())->isNotEmpty())
<div class="daily-game-profit-avatars">
@foreach ($dailyGameProfitLeaders as $leader)
<span class="daily-game-profit-avatar-wrap daily-game-profit-avatar-rank-{{ $leader->rank }}"
data-username="{{ $leader->username }}">
<img class="daily-game-profit-avatar" src="{{ $leader->headface_url }}" alt="{{ $leader->username }}"
onerror="this.src='/images/headface/1.gif'">
</span>
@endforeach
</div>
@else
<div class="daily-game-profit-empty">今日财神位待开张</div>
@endif
</div>
</div>
+70
View File
@@ -8,6 +8,7 @@
namespace Tests\Feature;
use App\Enums\CurrencySource;
use App\Events\MessageSent;
use App\Models\Department;
use App\Models\Gift;
@@ -16,6 +17,7 @@ use App\Models\Ride;
use App\Models\Room;
use App\Models\Sysparam;
use App\Models\User;
use App\Models\UserCurrencyLog;
use App\Models\UserPosition;
use App\Models\UserRidePurchase;
use App\Models\VipLevel;
@@ -67,6 +69,57 @@ class ChatControllerTest extends TestCase
$this->assertEquals(1, Redis::hexists("room:{$room->id}:users", $user->username));
}
/**
* 测试聊天室顶部会展示百家乐与赛马今日净盈利前三。
*/
public function test_room_header_displays_daily_game_profit_top_three(): void
{
Cache::flush();
Sysparam::updateOrCreate(['alias' => 'superlevel'], ['body' => '100']);
$room = Room::create(['room_name' => 'profitroom']);
$viewer = User::factory()->create(['username' => '围观群众', 'user_level' => 1]);
$first = User::factory()->create(['username' => '第一赢家', 'user_level' => 1]);
$second = User::factory()->create(['username' => '第二赢家', 'user_level' => 1]);
$third = User::factory()->create(['username' => '第三赢家', 'user_level' => 1]);
$yesterdayWinner = User::factory()->create(['username' => '昨日财神', 'user_level' => 1]);
$breakEvenUser = User::factory()->create(['username' => '白忙选手', 'user_level' => 1]);
$admin = User::factory()->create(['username' => '超级管理员', 'user_level' => 100]);
$bot = User::factory()->create(['username' => 'AI小班长', 'user_level' => 1]);
$this->recordGameGoldLog($first, CurrencySource::BACCARAT_WIN, 10000);
$this->recordGameGoldLog($first, CurrencySource::BACCARAT_BET, -2500);
$this->recordGameGoldLog($second, CurrencySource::HORSE_WIN, 6000);
$this->recordGameGoldLog($second, CurrencySource::HORSE_BET, -1000);
$this->recordGameGoldLog($third, CurrencySource::BACCARAT_WIN, 3000);
$this->recordGameGoldLog($yesterdayWinner, CurrencySource::HORSE_WIN, 50000, now()->subDay());
$this->recordGameGoldLog($breakEvenUser, CurrencySource::BACCARAT_WIN, 1000);
$this->recordGameGoldLog($breakEvenUser, CurrencySource::BACCARAT_BET, -1000);
$this->recordGameGoldLog($admin, CurrencySource::BACCARAT_WIN, 999999);
$this->recordGameGoldLog($bot, CurrencySource::HORSE_WIN, 888888);
$response = $this->actingAs($viewer)->get(route('chat.room', $room->id));
$leaders = $response->viewData('dailyGameProfitLeaders');
$this->assertSame(['超级管理员', '第一赢家', '第二赢家'], $leaders->pluck('username')->all());
$this->assertSame([999999, 7500, 5000], $leaders->pluck('net_profit')->all());
$this->assertStringStartsWith('/images/headface/', $leaders->first()->headface_url);
$response->assertOk();
$response->assertSee('今日游戏三杰');
$response->assertSee('class="daily-game-profit-avatar"', false);
$response->assertSee('data-username="超级管理员"', false);
$response->assertSee('data-username="第一赢家"', false);
$response->assertSee('data-username="第二赢家"', false);
$response->assertDontSee('金库爆破王');
$response->assertDontSee('马桌双修财神');
$response->assertDontSee('金币收割机');
$response->assertDontSee('+999,999');
$response->assertDontSee('+7,500');
$response->assertDontSee('+5,000');
}
/**
* 测试关闭房间会拒绝普通用户直接进入。
*/
@@ -1499,6 +1552,23 @@ class ChatControllerTest extends TestCase
$response->assertStatus(403);
}
/**
* 记录一条游戏金币流水,用于构造每日游戏净盈利榜测试数据。
*/
private function recordGameGoldLog(User $user, CurrencySource $source, int $amount, \DateTimeInterface|string|null $createdAt = null): void
{
UserCurrencyLog::forceCreate([
'user_id' => $user->id,
'username' => $user->username,
'currency' => 'gold',
'amount' => $amount,
'balance_after' => max(0, 100000 + $amount),
'source' => $source->value,
'remark' => '每日游戏净盈利榜测试',
'created_at' => $createdAt ?? now(),
]);
}
/**
* 创建带指定聊天室权限的在职职务用户。
*