增加每日游戏三杰榜
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带指定聊天室权限的在职职务用户。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user