From f4a632a9c11d200000e5d0f75f810d34953fd138 Mon Sep 17 00:00:00 2001 From: lkddi Date: Sat, 11 Apr 2026 23:43:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=99=BE=E5=AE=B6=E4=B9=90?= =?UTF-8?q?=E4=B9=B0=E5=8D=95=E8=A1=A5=E5=81=BF=E8=87=AA=E5=8A=A8=E9=A2=86?= =?UTF-8?q?=E5=8F=96=E4=B8=8E=E8=81=8A=E5=A4=A9=E5=AE=A4=E6=92=AD=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Jobs/AiClaimBaccaratLossCoverJob.php | 97 +++++++++++++++++++ app/Services/BaccaratLossCoverService.php | 75 +++++++++++++- .../BaccaratLossCoverControllerTest.php | 97 +++++++++++++++++++ 3 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 app/Jobs/AiClaimBaccaratLossCoverJob.php diff --git a/app/Jobs/AiClaimBaccaratLossCoverJob.php b/app/Jobs/AiClaimBaccaratLossCoverJob.php new file mode 100644 index 0000000..81d22ba --- /dev/null +++ b/app/Jobs/AiClaimBaccaratLossCoverJob.php @@ -0,0 +1,97 @@ +find($this->eventId); + if (! $event) { + Log::channel('daily')->warning('AI小班长自动领取买单补偿失败:活动不存在', [ + 'event_id' => $this->eventId, + ]); + + return; + } + + // 只有活动进入可领取状态后,才允许自动发起补偿领取。 + if (! $event->isClaimable()) { + Log::channel('daily')->info('AI小班长自动领取买单补偿跳过:活动暂不可领取', [ + 'event_id' => $event->id, + 'status' => $event->status, + ]); + + return; + } + + $aiUser = User::query()->where('username', 'AI小班长')->first(); + if (! $aiUser) { + Log::channel('daily')->warning('AI小班长自动领取买单补偿失败:未找到 AI 用户', [ + 'event_id' => $event->id, + ]); + + return; + } + + $record = BaccaratLossCoverRecord::query() + ->where('event_id', $event->id) + ->where('user_id', $aiUser->id) + ->first(); + + // 没有补偿记录或本次没有待领取金额时,直接记日志后结束。 + if (! $record || $record->compensation_amount <= 0 || $record->claim_status !== 'pending') { + Log::channel('daily')->info('AI小班长自动领取买单补偿跳过:暂无待领取补偿', [ + 'event_id' => $event->id, + 'user_id' => $aiUser->id, + 'claim_status' => $record?->claim_status, + 'compensation_amount' => $record?->compensation_amount, + ]); + + return; + } + + $claimResult = $lossCoverService->claim($event, $aiUser); + + // 统一记录自动领取结果,便于后续核对 AI 补偿发放情况。 + Log::channel('daily')->info('AI小班长自动领取买单补偿结果', [ + 'event_id' => $event->id, + 'user_id' => $aiUser->id, + 'ok' => $claimResult['ok'], + 'message' => $claimResult['message'], + 'amount' => $claimResult['amount'] ?? 0, + ]); + } +} diff --git a/app/Services/BaccaratLossCoverService.php b/app/Services/BaccaratLossCoverService.php index bb1437c..93301ed 100644 --- a/app/Services/BaccaratLossCoverService.php +++ b/app/Services/BaccaratLossCoverService.php @@ -11,6 +11,7 @@ namespace App\Services; use App\Enums\CurrencySource; use App\Events\MessageSent; +use App\Jobs\AiClaimBaccaratLossCoverJob; use App\Jobs\SaveMessageJob; use App\Models\BaccaratBet; use App\Models\BaccaratLossCoverEvent; @@ -198,7 +199,7 @@ class BaccaratLossCoverService return ['ok' => false, 'message' => '当前活动暂未开放领取,或已超过领取时间。']; } - return DB::transaction(function () use ($event, $user): array { + $result = DB::transaction(function () use ($event, $user): array { $record = BaccaratLossCoverRecord::query() ->where('event_id', $event->id) ->where('user_id', $user->id) @@ -247,6 +248,13 @@ class BaccaratLossCoverService 'amount' => $amount, ]; }); + + // 领取成功后,需要向聊天室广播一条同款百家乐风格消息,方便其他人快速领取。 + if (($result['ok'] ?? false) === true && isset($result['amount'])) { + $this->pushClaimMessage($event->fresh(), $user, (int) $result['amount']); + } + + return $result; } /** @@ -396,6 +404,9 @@ class BaccaratLossCoverService $this->pushRoomMessage($content, '#7c3aed'); $event->update(['ended_notice_sent_at' => now()]); + + // 活动开放领取后,为 AI小班长补发一次自动领取任务。 + $this->dispatchAiAutoClaimJob($event->fresh()); } /** @@ -419,4 +430,66 @@ class BaccaratLossCoverService broadcast(new MessageSent(1, $message)); SaveMessageJob::dispatch($message); } + + /** + * 在用户领取补偿成功后,向聊天室广播领取播报。 + */ + private function pushClaimMessage(?BaccaratLossCoverEvent $event, User $user, int $amount): void + { + if (! $event) { + return; + } + + $formattedAmount = number_format($amount); + $button = $event->status === 'claimable' + ? ' ' + : ''; + + // 领取成功的公屏格式复用百家乐参与播报风格,保证聊天室感知一致。 + $content = "🌟 🎲 {$user->username} 领取了 {$formattedAmount} 金币补偿!✨{$button}"; + $message = [ + 'id' => $this->chatState->nextMessageId(1), + 'room_id' => 1, + 'from_user' => '系统传音', + 'to_user' => '大家', + 'content' => $content, + 'is_secret' => false, + 'font_color' => '#d97706', + 'action' => '', + 'sent_at' => now()->toDateTimeString(), + ]; + + $this->chatState->pushMessage(1, $message); + broadcast(new MessageSent(1, $message)); + SaveMessageJob::dispatch($message); + } + + /** + * 在活动可领取后,为 AI小班长派发自动领取补偿任务。 + */ + private function dispatchAiAutoClaimJob(?BaccaratLossCoverEvent $event): void + { + if (! $event || $event->status !== 'claimable') { + return; + } + + $aiUserId = User::query()->where('username', 'AI小班长')->value('id'); + if (! $aiUserId) { + return; + } + + $hasPendingRecord = BaccaratLossCoverRecord::query() + ->where('event_id', $event->id) + ->where('user_id', $aiUserId) + ->where('claim_status', 'pending') + ->where('compensation_amount', '>', 0) + ->exists(); + + // 只有 AI小班长确实存在待领取补偿时,才派发自动领取任务。 + if (! $hasPendingRecord) { + return; + } + + AiClaimBaccaratLossCoverJob::dispatch($event->id); + } } diff --git a/tests/Feature/BaccaratLossCoverControllerTest.php b/tests/Feature/BaccaratLossCoverControllerTest.php index 991e604..d563645 100644 --- a/tests/Feature/BaccaratLossCoverControllerTest.php +++ b/tests/Feature/BaccaratLossCoverControllerTest.php @@ -10,11 +10,15 @@ namespace Tests\Feature; use App\Enums\CurrencySource; +use App\Events\MessageSent; +use App\Jobs\AiClaimBaccaratLossCoverJob; +use App\Jobs\SaveMessageJob; use App\Models\BaccaratLossCoverEvent; use App\Models\BaccaratLossCoverRecord; use App\Models\BaccaratRound; use App\Models\GameConfig; use App\Models\User; +use App\Services\BaccaratLossCoverService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Queue; @@ -133,6 +137,9 @@ class BaccaratLossCoverControllerTest extends TestCase */ public function test_user_can_claim_baccarat_loss_cover_and_currency_log_is_written(): void { + Event::fake([MessageSent::class]); + Queue::fake([SaveMessageJob::class]); + $user = User::factory()->create(['jjb' => 200]); $event = BaccaratLossCoverEvent::factory()->create([ 'status' => 'claimable', @@ -171,6 +178,12 @@ class BaccaratLossCoverControllerTest extends TestCase 'amount' => 300, 'source' => CurrencySource::BACCARAT_LOSS_COVER_CLAIM->value, ]); + + Event::assertDispatched(MessageSent::class); + Queue::assertPushed(SaveMessageJob::class, function (SaveMessageJob $job): bool { + return str_contains($job->messageData['content'], '领取了 300 金币补偿') + && str_contains($job->messageData['content'], 'claimBaccaratLossCover'); + }); } /** @@ -200,4 +213,88 @@ class BaccaratLossCoverControllerTest extends TestCase $response->assertJsonPath('events.0.my_record.claim_status', 'claimed'); $response->assertJsonPath('events.0.my_record.claimed_amount', 400); } + + /** + * 验证活动进入可领取状态后会为 AI 小班长派发自动领取任务。 + */ + public function test_claimable_event_dispatches_ai_auto_claim_job(): void + { + Queue::fake(); + + $aiUser = User::factory()->create([ + 'username' => 'AI小班长', + 'jjb' => 1000, + ]); + + $event = BaccaratLossCoverEvent::factory()->create([ + 'status' => 'active', + 'starts_at' => now()->subHour(), + 'ends_at' => now()->subMinute(), + 'claim_deadline_at' => now()->addHours(12), + ]); + + BaccaratLossCoverRecord::factory()->create([ + 'event_id' => $event->id, + 'user_id' => $aiUser->id, + 'compensation_amount' => 260, + 'total_loss_amount' => 260, + 'claim_status' => 'pending', + ]); + + app(BaccaratLossCoverService::class)->closeDueActiveEvents(); + + Queue::assertPushed(AiClaimBaccaratLossCoverJob::class, function (AiClaimBaccaratLossCoverJob $job) use ($event): bool { + return $job->eventId === $event->id; + }); + + $this->assertDatabaseHas('baccarat_loss_cover_events', [ + 'id' => $event->id, + 'status' => 'claimable', + ]); + } + + /** + * 验证 AI 小班长自动领取任务会发放补偿并写入金币流水。 + */ + public function test_ai_auto_claim_job_claims_compensation_and_writes_currency_log(): void + { + $aiUser = User::factory()->create([ + 'username' => 'AI小班长', + 'jjb' => 200, + ]); + + $event = BaccaratLossCoverEvent::factory()->create([ + 'status' => 'claimable', + 'claim_deadline_at' => now()->addHours(12), + 'total_loss_amount' => 320, + ]); + + BaccaratLossCoverRecord::factory()->create([ + 'event_id' => $event->id, + 'user_id' => $aiUser->id, + 'total_bet_amount' => 600, + 'total_loss_amount' => 320, + 'compensation_amount' => 320, + 'claim_status' => 'pending', + ]); + + $job = new AiClaimBaccaratLossCoverJob($event->id); + $job->handle(app(BaccaratLossCoverService::class)); + + $this->assertSame(520, (int) $aiUser->fresh()->jjb); + + $this->assertDatabaseHas('baccarat_loss_cover_records', [ + 'event_id' => $event->id, + 'user_id' => $aiUser->id, + 'claim_status' => 'claimed', + 'claimed_amount' => 320, + ]); + + $this->assertDatabaseHas('user_currency_logs', [ + 'user_id' => $aiUser->id, + 'currency' => 'gold', + 'amount' => 320, + 'source' => CurrencySource::BACCARAT_LOSS_COVER_CLAIM->value, + ]); + } }