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,
+ ]);
+ }
}