赛马优化通知
This commit is contained in:
@@ -93,12 +93,14 @@ class CloseHorseRaceJob implements ShouldQueue
|
|||||||
->get();
|
->get();
|
||||||
|
|
||||||
$totalPayout = 0;
|
$totalPayout = 0;
|
||||||
|
$participantSettlements = [];
|
||||||
|
|
||||||
DB::transaction(function () use ($bets, $winnerId, $distributablePool, $winnerPool, $currency, &$totalPayout) {
|
DB::transaction(function () use ($bets, $winnerId, $distributablePool, $winnerPool, $currency, &$totalPayout, &$participantSettlements) {
|
||||||
foreach ($bets as $bet) {
|
foreach ($bets as $bet) {
|
||||||
if ((int) $bet->horse_id !== $winnerId) {
|
if ((int) $bet->horse_id !== $winnerId) {
|
||||||
// 未中奖(本金已在下注时扣除)
|
// 未中奖(本金已在下注时扣除)
|
||||||
$bet->update(['status' => 'lost', 'payout' => 0]);
|
$bet->update(['status' => 'lost', 'payout' => 0]);
|
||||||
|
$this->recordParticipantSettlement($participantSettlements, $bet, -$bet->amount, 0);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -122,6 +124,8 @@ class CloseHorseRaceJob implements ShouldQueue
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 结算提示需要显示本场净输赢,因此要减去下注时已支付的本金。
|
||||||
|
$this->recordParticipantSettlement($participantSettlements, $bet, $payout - $bet->amount, $payout);
|
||||||
$totalPayout += $payout;
|
$totalPayout += $payout;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -129,24 +133,111 @@ class CloseHorseRaceJob implements ShouldQueue
|
|||||||
// 公屏公告
|
// 公屏公告
|
||||||
$this->pushResultMessage($race, $chatState, $totalPayout);
|
$this->pushResultMessage($race, $chatState, $totalPayout);
|
||||||
|
|
||||||
|
// 参与者私聊结算提醒
|
||||||
|
$this->pushParticipantToastNotifications($race, $chatState, $participantSettlements);
|
||||||
|
|
||||||
// 广播结算事件
|
// 广播结算事件
|
||||||
broadcast(new HorseRaceSettled($race));
|
broadcast(new HorseRaceSettled($race));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汇总单个参与者本场的下注、返还与净输赢金额。
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>> $participantSettlements
|
||||||
|
*/
|
||||||
|
private function recordParticipantSettlement(array &$participantSettlements, HorseBet $bet, int $netChange, int $payout): void
|
||||||
|
{
|
||||||
|
if (! $bet->user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = (int) $bet->user->id;
|
||||||
|
$existing = $participantSettlements[$userId] ?? [
|
||||||
|
'user' => $bet->user,
|
||||||
|
'username' => $bet->user->username,
|
||||||
|
'bet_amount' => 0,
|
||||||
|
'payout' => 0,
|
||||||
|
'net_change' => 0,
|
||||||
|
'horse_id' => (int) $bet->horse_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 即使出现脏数据导致同一用户多笔下注,也统一汇总成本场总输赢。
|
||||||
|
$existing['bet_amount'] += (int) $bet->amount;
|
||||||
|
$existing['payout'] += $payout;
|
||||||
|
$existing['net_change'] += $netChange;
|
||||||
|
$existing['horse_id'] = (int) $bet->horse_id;
|
||||||
|
|
||||||
|
$participantSettlements[$userId] = $existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向参与本场的用户发送私聊结算提示,并复用右下角 toast 通知。
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>> $participantSettlements
|
||||||
|
*/
|
||||||
|
private function pushParticipantToastNotifications(HorseRace $race, ChatStateService $chatState, array $participantSettlements): void
|
||||||
|
{
|
||||||
|
if ($participantSettlements === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roomId = 1;
|
||||||
|
$winnerName = $this->resolveWinnerHorseName($race);
|
||||||
|
|
||||||
|
foreach ($participantSettlements as $settlement) {
|
||||||
|
$user = $settlement['user'];
|
||||||
|
$username = (string) $settlement['username'];
|
||||||
|
$betAmount = (int) $settlement['bet_amount'];
|
||||||
|
$netChange = (int) $settlement['net_change'];
|
||||||
|
$freshGold = (int) ($user->fresh()->jjb ?? 0);
|
||||||
|
$horseId = (int) $settlement['horse_id'];
|
||||||
|
|
||||||
|
$absNetChange = number_format(abs($netChange));
|
||||||
|
$betAmountText = number_format($betAmount);
|
||||||
|
$summaryText = $netChange > 0
|
||||||
|
? "净赢 {$absNetChange} 金币"
|
||||||
|
: ($netChange < 0 ? "净输 {$absNetChange} 金币" : '不输不赢');
|
||||||
|
$toastIcon = $netChange > 0 ? '🏇' : ($netChange < 0 ? '📉' : '🐎');
|
||||||
|
$toastColor = $netChange > 0 ? '#10b981' : ($netChange < 0 ? '#ef4444' : '#3b82f6');
|
||||||
|
$toastMessage = $netChange > 0
|
||||||
|
? "冠军:<b>{$winnerName}</b><br>你本场净赢 <b>+{$absNetChange}</b> 金币!"
|
||||||
|
: ($netChange < 0
|
||||||
|
? "冠军:<b>{$winnerName}</b><br>你本场净输 <b>-{$absNetChange}</b> 金币。"
|
||||||
|
: "冠军:<b>{$winnerName}</b><br>你本场不输不赢。");
|
||||||
|
|
||||||
|
// 写入系统私聊,方便用户在聊天历史中回看本场结算结果。
|
||||||
|
$msg = [
|
||||||
|
'id' => $chatState->nextMessageId($roomId),
|
||||||
|
'room_id' => $roomId,
|
||||||
|
'from_user' => '系统',
|
||||||
|
'to_user' => $username,
|
||||||
|
'content' => "🏇 赛马第 #{$race->id} 场已结束,冠军:{$winnerName}。你押注 {$horseId} 号马 {$betAmountText} 金币,{$summaryText};当前金币:{$freshGold} 枚。",
|
||||||
|
'is_secret' => true,
|
||||||
|
'font_color' => '#f59e0b',
|
||||||
|
'action' => '',
|
||||||
|
'sent_at' => now()->toDateTimeString(),
|
||||||
|
'toast_notification' => [
|
||||||
|
'title' => '🏇 赛马本场结算',
|
||||||
|
'message' => $toastMessage,
|
||||||
|
'icon' => $toastIcon,
|
||||||
|
'color' => $toastColor,
|
||||||
|
'duration' => 10000,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$chatState->pushMessage($roomId, $msg);
|
||||||
|
broadcast(new MessageSent($roomId, $msg));
|
||||||
|
SaveMessageJob::dispatch($msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 向公屏发送赛果系统消息。
|
* 向公屏发送赛果系统消息。
|
||||||
*/
|
*/
|
||||||
private function pushResultMessage(HorseRace $race, ChatStateService $chatState, int $totalPayout): void
|
private function pushResultMessage(HorseRace $race, ChatStateService $chatState, int $totalPayout): void
|
||||||
{
|
{
|
||||||
// 找出胜利马匹名称
|
// 找出胜利马匹名称
|
||||||
$horses = $race->horses ?? [];
|
$winnerName = $this->resolveWinnerHorseName($race);
|
||||||
$winnerName = '未知';
|
|
||||||
foreach ($horses as $horse) {
|
|
||||||
if (($horse['id'] ?? 0) === (int) $race->winner_horse_id) {
|
|
||||||
$winnerName = ($horse['emoji'] ?? '').($horse['name'] ?? '');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$payoutText = $totalPayout > 0
|
$payoutText = $totalPayout > 0
|
||||||
? '共派发 💰'.number_format($totalPayout).' 金币'
|
? '共派发 💰'.number_format($totalPayout).' 金币'
|
||||||
@@ -169,4 +260,19 @@ class CloseHorseRaceJob implements ShouldQueue
|
|||||||
broadcast(new MessageSent(1, $msg));
|
broadcast(new MessageSent(1, $msg));
|
||||||
SaveMessageJob::dispatch($msg);
|
SaveMessageJob::dispatch($msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析冠军马匹的展示名称。
|
||||||
|
*/
|
||||||
|
private function resolveWinnerHorseName(HorseRace $race): string
|
||||||
|
{
|
||||||
|
$horses = $race->horses ?? [];
|
||||||
|
foreach ($horses as $horse) {
|
||||||
|
if (($horse['id'] ?? 0) === (int) $race->winner_horse_id) {
|
||||||
|
return ($horse['emoji'] ?? '').($horse['name'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace Tests\Feature;
|
|||||||
|
|
||||||
use App\Events\HorseRaceSettled;
|
use App\Events\HorseRaceSettled;
|
||||||
use App\Jobs\CloseHorseRaceJob;
|
use App\Jobs\CloseHorseRaceJob;
|
||||||
|
use App\Jobs\SaveMessageJob;
|
||||||
use App\Models\GameConfig;
|
use App\Models\GameConfig;
|
||||||
use App\Models\HorseBet;
|
use App\Models\HorseBet;
|
||||||
use App\Models\HorseRace;
|
use App\Models\HorseRace;
|
||||||
@@ -19,6 +20,7 @@ use App\Services\ChatStateService;
|
|||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Facades\Queue;
|
use Illuminate\Support\Facades\Queue;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -414,4 +416,61 @@ class HorseRaceControllerTest extends TestCase
|
|||||||
$this->assertSame(5000, $payload['winner_pool']);
|
$this->assertSame(5000, $payload['winner_pool']);
|
||||||
$this->assertSame(9500, $payload['distributable_pool']);
|
$this->assertSame(9500, $payload['distributable_pool']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 方法功能:验证赛马开奖后会给参与者发送带右下角提示的私聊结算通知。
|
||||||
|
*/
|
||||||
|
public function test_settlement_pushes_private_toast_notification_to_participant(): void
|
||||||
|
{
|
||||||
|
Queue::fake();
|
||||||
|
|
||||||
|
/** @var \App\Models\User $user */
|
||||||
|
$user = User::factory()->create(['jjb' => 500]);
|
||||||
|
|
||||||
|
$race = HorseRace::create([
|
||||||
|
'status' => 'running',
|
||||||
|
'bet_opens_at' => now()->subMinutes(2),
|
||||||
|
'bet_closes_at' => now()->subMinute(),
|
||||||
|
'race_starts_at' => now()->subSeconds(30),
|
||||||
|
'winner_horse_id' => 1,
|
||||||
|
'horses' => [
|
||||||
|
['id' => 1, 'name' => 'Horse A', 'emoji' => '🐎'],
|
||||||
|
['id' => 2, 'name' => 'Horse B', 'emoji' => '🏇'],
|
||||||
|
],
|
||||||
|
'total_pool' => 100,
|
||||||
|
]);
|
||||||
|
|
||||||
|
HorseBet::create([
|
||||||
|
'race_id' => $race->id,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'horse_id' => 2,
|
||||||
|
'amount' => 100,
|
||||||
|
'status' => 'pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 模拟下注时已扣款,便于校验结算通知中的净输赢语义。
|
||||||
|
$user->decrement('jjb', 100);
|
||||||
|
|
||||||
|
(new CloseHorseRaceJob($race))->handle(
|
||||||
|
app(\App\Services\UserCurrencyService::class),
|
||||||
|
app(ChatStateService::class),
|
||||||
|
);
|
||||||
|
|
||||||
|
$messages = Redis::lrange('room:1:messages', 0, -1);
|
||||||
|
$privateMessage = collect($messages)
|
||||||
|
->map(fn (string $item) => json_decode($item, true))
|
||||||
|
->first(fn (array $item) => ($item['from_user'] ?? null) === '系统'
|
||||||
|
&& ($item['to_user'] ?? null) === $user->username
|
||||||
|
&& ($item['toast_notification']['title'] ?? null) === '🏇 赛马本场结算');
|
||||||
|
|
||||||
|
$this->assertNotNull($privateMessage);
|
||||||
|
$this->assertTrue((bool) ($privateMessage['is_secret'] ?? false));
|
||||||
|
$this->assertStringContainsString('净输 100 金币', (string) ($privateMessage['content'] ?? ''));
|
||||||
|
$this->assertStringContainsString('-100', (string) ($privateMessage['toast_notification']['message'] ?? ''));
|
||||||
|
$this->assertSame('📉', $privateMessage['toast_notification']['icon'] ?? null);
|
||||||
|
$this->assertSame('#ef4444', $privateMessage['toast_notification']['color'] ?? null);
|
||||||
|
|
||||||
|
// 公屏赛果消息 + 参与者私聊结算通知,都应进入落库队列。
|
||||||
|
Queue::assertPushed(SaveMessageJob::class, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user