'baccarat'], [ 'name' => 'Baccarat', 'icon' => 'baccarat', 'description' => 'Baccarat Game', 'enabled' => true, 'params' => [ 'min_bet' => 100, 'max_bet' => 50000, ], ] ); } /** * 验证 superlevel 管理员可以创建百家乐买单活动。 */ public function test_superlevel_admin_can_create_baccarat_loss_cover_event(): void { Queue::fake(); $admin = User::factory()->create(['id' => 1, 'user_level' => 100]); $response = $this->actingAs($admin)->postJson(route('command.baccarat_loss_cover.store'), [ 'title' => '你玩游戏我买单', 'description' => '测试活动', 'starts_at' => now()->addMinutes(5)->toDateTimeString(), 'ends_at' => now()->addMinutes(35)->toDateTimeString(), 'claim_deadline_at' => now()->addDay()->toDateTimeString(), ]); $response->assertOk()->assertJson(['ok' => true]); $this->assertDatabaseHas('baccarat_loss_cover_events', [ 'title' => '你玩游戏我买单', 'created_by_user_id' => $admin->id, 'status' => 'scheduled', ]); } /** * 验证拥有买单活动权限的职务用户也可以创建活动。 */ public function test_position_user_with_loss_cover_permission_can_create_event(): void { Queue::fake(); $admin = $this->createUserWithPermissions([ PositionPermissionRegistry::ROOM_BACCARAT_LOSS_COVER, ]); $response = $this->actingAs($admin)->postJson(route('command.baccarat_loss_cover.store'), [ 'title' => '职务买单活动', 'description' => '职务权限测试', 'starts_at' => now()->addMinutes(3)->toDateTimeString(), 'ends_at' => now()->addMinutes(33)->toDateTimeString(), 'claim_deadline_at' => now()->addHours(6)->toDateTimeString(), ]); $response->assertOk()->assertJson(['ok' => true]); } /** * 验证高等级但无买单活动权限的用户会被拒绝。 */ public function test_high_level_user_without_loss_cover_permission_cannot_create_event(): void { $admin = User::factory()->create(['user_level' => 100]); $response = $this->actingAs($admin)->postJson(route('command.baccarat_loss_cover.store'), [ 'title' => '无权限活动', 'description' => '测试活动', 'starts_at' => now()->addMinutes(5)->toDateTimeString(), 'ends_at' => now()->addMinutes(35)->toDateTimeString(), 'claim_deadline_at' => now()->addDay()->toDateTimeString(), ]); $response->assertStatus(403); } /** * 验证活动进行中下注会挂到活动并写入用户聚合记录。 */ public function test_bet_during_active_event_is_tracked_in_loss_cover_record(): void { Event::fake(); $user = User::factory()->create(['jjb' => 500]); $event = BaccaratLossCoverEvent::factory()->create([ 'status' => 'active', 'starts_at' => now()->subMinutes(2), 'ends_at' => now()->addMinutes(20), ]); $round = BaccaratRound::forceCreate([ 'status' => 'betting', 'bet_opens_at' => now(), 'bet_closes_at' => now()->addMinutes(1), 'total_bet_big' => 0, 'total_bet_small' => 0, 'total_bet_triple' => 0, 'bet_count' => 0, 'bet_count_big' => 0, 'bet_count_small' => 0, 'bet_count_triple' => 0, 'total_payout' => 0, ]); $response = $this->actingAs($user)->postJson(route('baccarat.bet'), [ 'round_id' => $round->id, 'bet_type' => 'big', 'amount' => 100, ]); $response->assertOk()->assertJson(['ok' => true]); $this->assertDatabaseHas('baccarat_bets', [ 'round_id' => $round->id, 'user_id' => $user->id, 'loss_cover_event_id' => $event->id, 'amount' => 100, ]); $this->assertDatabaseHas('baccarat_loss_cover_records', [ 'event_id' => $event->id, 'user_id' => $user->id, 'total_bet_amount' => 100, 'claim_status' => 'not_eligible', ]); $this->assertDatabaseHas('baccarat_loss_cover_events', [ 'id' => $event->id, 'participant_count' => 1, ]); } /** * 验证用户领取补偿后会增加金币并写入金币流水。 */ 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', 'claim_deadline_at' => now()->addHours(12), 'total_loss_amount' => 300, ]); BaccaratLossCoverRecord::factory()->create([ 'event_id' => $event->id, 'user_id' => $user->id, 'total_bet_amount' => 600, 'total_loss_amount' => 300, 'compensation_amount' => 300, 'claim_status' => 'pending', ]); // 额外保留一条待领取记录,确保当前用户领取后活动仍处于 claimable, // 从而广播消息继续携带“领取补偿”按钮。 BaccaratLossCoverRecord::factory()->create([ 'event_id' => $event->id, 'user_id' => User::factory()->create()->id, 'total_bet_amount' => 800, 'total_loss_amount' => 200, 'compensation_amount' => 200, 'claim_status' => 'pending', ]); $response = $this->actingAs($user)->postJson(route('baccarat-loss-cover.claim', $event)); $response->assertOk()->assertJson([ 'ok' => true, 'amount' => 300, ]); $this->assertSame(500, (int) $user->fresh()->jjb); $this->assertDatabaseHas('baccarat_loss_cover_records', [ 'event_id' => $event->id, 'user_id' => $user->id, 'claim_status' => 'claimed', 'claimed_amount' => 300, ]); $this->assertDatabaseHas('user_currency_logs', [ 'user_id' => $user->id, 'currency' => 'gold', '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'); }); } /** * 验证历史接口会返回当前用户在活动中的领取状态。 */ public function test_history_endpoint_contains_my_claim_status(): void { $user = User::factory()->create(); $event = BaccaratLossCoverEvent::factory()->create([ 'status' => 'completed', 'total_claimed_amount' => 400, ]); BaccaratLossCoverRecord::factory()->create([ 'event_id' => $event->id, 'user_id' => $user->id, 'claim_status' => 'claimed', 'claimed_amount' => 400, 'compensation_amount' => 400, 'claimed_at' => now(), ]); $response = $this->actingAs($user)->getJson(route('baccarat-loss-cover.history')); $response->assertOk(); $response->assertJsonPath('events.0.id', $event->id); $response->assertJsonPath('events.0.my_record.claim_status', 'claimed'); $response->assertJsonPath('events.0.my_record.claimed_amount', 400); } /** * 验证“当前活动”页签摘要不会把可领取中的历史活动当成当前活动。 */ public function test_overview_summary_ignores_claimable_history_event(): void { $user = User::factory()->create(); $event = BaccaratLossCoverEvent::factory()->create([ 'status' => 'claimable', 'starts_at' => now()->subHour(), 'ends_at' => now()->subMinutes(20), 'claim_deadline_at' => now()->addHours(12), ]); $defaultResponse = $this->actingAs($user)->getJson(route('baccarat-loss-cover.summary')); $defaultResponse->assertOk(); $defaultResponse->assertJsonPath('event.id', $event->id); $defaultResponse->assertJsonPath('event.status', 'claimable'); $overviewResponse = $this->actingAs($user)->getJson(route('baccarat-loss-cover.summary', ['scene' => 'overview'])); $overviewResponse->assertOk(); $overviewResponse->assertJsonPath('event', null); } /** * 验证活动进入可领取状态后会为 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, ]); } /** * 创建带指定聊天室权限的在职职务用户。 * * @param list $permissions */ private function createUserWithPermissions(array $permissions): User { $user = User::factory()->create([ 'user_level' => 90, ]); $department = Department::create([ 'name' => '买单活动部'.$user->id, 'rank' => 90, 'color' => '#15803d', 'sort_order' => 1, 'description' => '买单活动权限测试', ]); $position = Position::create([ 'department_id' => $department->id, 'name' => '买单活动职务'.$user->id, 'icon' => '🎁', 'rank' => 90, 'level' => 90, 'sort_order' => 1, 'permissions' => $permissions, ]); UserPosition::create([ 'user_id' => $user->id, 'position_id' => $position->id, 'appointed_by_user_id' => null, 'appointed_at' => now(), 'remark' => '买单活动权限测试', 'is_active' => true, ]); return $user->fresh(); } }