diff --git a/app/Events/RedPacketClaimed.php b/app/Events/RedPacketClaimed.php index a3fc412..b79532e 100644 --- a/app/Events/RedPacketClaimed.php +++ b/app/Events/RedPacketClaimed.php @@ -1,10 +1,10 @@ */ public function broadcastOn(): array { - return [new PrivateChannel('user.'.$this->claimer->id)]; + return [ + new PresenceChannel('room.'.$this->roomId), + new PrivateChannel('user.'.$this->claimer->id), + ]; } /** + * 广播领取结果与剩余份数。 + * * @return array */ public function broadcastWith(): array { + $typeLabel = $this->type === 'exp' ? '经验' : '金币'; + return [ 'envelope_id' => $this->envelopeId, + 'claimer_id' => $this->claimer->id, + 'claimer_username' => $this->claimer->username, 'amount' => $this->amount, - 'message' => "🧧 成功抢到 {$this->amount} 金币礼包!", + 'remaining_count' => $this->remainingCount, + 'type' => $this->type, + 'message' => "🧧 成功抢到 {$this->amount} {$typeLabel}礼包!", ]; } diff --git a/app/Http/Controllers/RedPacketController.php b/app/Http/Controllers/RedPacketController.php index 50b5a02..11f0851 100644 --- a/app/Http/Controllers/RedPacketController.php +++ b/app/Http/Controllers/RedPacketController.php @@ -32,6 +32,11 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +/** + * 类功能:处理聊天室礼包的发包、查状态与抢包流程 + * + * 负责礼包主记录创建、Redis 拆包金额管理、领取入账以及实时广播。 + */ class RedPacketController extends Controller { /** 礼包固定总数量 */ @@ -307,8 +312,19 @@ class RedPacketController extends Controller return response()->json(['status' => 'error', 'message' => '您已经领过这个礼包了'], 422); } - // 广播领取事件(给自己的私有频道,前端弹 Toast) - broadcast(new RedPacketClaimed($user, $amount, $envelope->id)); + // 重新读取红包统计,确保广播与响应使用的是最新剩余份数。 + $envelope->refresh(); + $remainingCount = $envelope->remainingCount(); + + // 广播领取事件:房间内所有在线用户实时刷新剩余份数,领取者本人同步收到到账通知。 + broadcast(new RedPacketClaimed( + claimer: $user, + amount: $amount, + envelopeId: $envelope->id, + roomId: $envelope->room_id, + remainingCount: $remainingCount, + type: $envelopeType, + )); // 在聊天室发送领取播报(所有人可见) $typeLabel = $envelopeType === 'exp' ? '经验' : '金币'; @@ -335,6 +351,7 @@ class RedPacketController extends Controller 'status' => 'success', 'amount' => $amount, 'type' => $envelopeType, + 'remaining_count' => $remainingCount, 'message' => "🧧 恭喜!您抢到了 {$amount} {$typeLabel}!当前{$typeLabel}:{$balanceNow}。", ]); } diff --git a/resources/views/chat/partials/games/red-packet-panel.blade.php b/resources/views/chat/partials/games/red-packet-panel.blade.php index a3fc6c0..acfbcf7 100644 --- a/resources/views/chat/partials/games/red-packet-panel.blade.php +++ b/resources/views/chat/partials/games/red-packet-panel.blade.php @@ -279,7 +279,7 @@ * 2. showRedPacketModal() — 收到 RedPacketSent 事件后弹出红包卡片 * 3. claimRedPacket() — 用户点击「立即抢红包」 * 4. closeRedPacketModal() — 关闭红包弹窗 - * 5. WebSocket 监听 — 监听 red-packet.sent 广播事件 + * 5. WebSocket 监听 — 监听 red-packet.sent / red-packet.claimed 广播事件 */ (function() { 'use strict'; @@ -616,6 +616,10 @@ btn.textContent = '🎉 已抢到!'; statusEl.style.color = '#16a34a'; statusEl.textContent = `恭喜!您抢到了 ${data.amount} ${typeLabel}!`; + const remainingEl = document.getElementById('rp-remaining'); + if (remainingEl && typeof data.remaining_count === 'number') { + remainingEl.textContent = data.remaining_count; + } // 弹出全局 Toast window.chatToast.show({ @@ -631,9 +635,17 @@ } else { statusEl.style.color = '#dc2626'; statusEl.textContent = data.message || '抢包失败'; - // 若已领过或已抢完则禁用按钮,否则解除禁用以重试 - if (data.message && (data.message.includes('已经领过') || data.message.includes('已被抢完') || - data.message.includes('已抢完'))) { + const message = data.message || ''; + const shouldAutoClose = message.includes('已过期') + || message.includes('已被抢完') + || message.includes('已抢完') + || message.includes('红包已抢完或已过期'); + + // 若红包已经结束,则保持禁用并在 3 秒后自动关闭弹窗。 + if (shouldAutoClose) { + btn.textContent = '礼包已结束'; + setTimeout(() => closeRedPacketModal(), 3000); + } else if (message.includes('已经领过')) { btn.textContent = '已参与'; } else { btn.disabled = false; @@ -652,11 +664,12 @@ /** * 收到「系统传音」中的领取播报时,同步更新弹窗内的名单与剩余数。 * - * @param {string} username 领取者用户名 - * @param {number} amount 领取金额 - * @param {number} remaining 剩余份数 + * @param {string} username 领取者用户名 + * @param {number} amount 领取金额 + * @param {number} remaining 剩余份数 + * @param {'gold'|'exp'} [type] 红包类型 */ - window.updateRedPacketClaimsUI = function(username, amount, remaining) { + window.updateRedPacketClaimsUI = function(username, amount, remaining, type = _rpType) { const remainingEl = document.getElementById('rp-remaining'); if (remainingEl) { remainingEl.textContent = remaining; @@ -669,9 +682,10 @@ } listEl.style.display = 'block'; + const typeLabel = type === 'exp' ? '经验' : '金币'; const item = document.createElement('div'); item.className = 'rp-claim-item'; - item.innerHTML = `${username}+${amount} 金币`; + item.innerHTML = `${username}+${amount} ${typeLabel}`; itemsEl.prepend(item); // 若已全部领完,更新按钮状态 @@ -687,10 +701,10 @@ } }; - // ── WebSocket 监听 red-packet.sent ─────────────── + // ── WebSocket 监听 red-packet.sent / red-packet.claimed ─────────────── /** - * 等待 Echo 就绪后注册 red-packet.sent 事件监听, - * 每次收到新红包时弹出红包卡片弹窗。 + * 等待 Echo 就绪后注册红包相关事件监听, + * 新红包弹窗展示,领取成功后实时刷新剩余份数。 */ function setupRedPacketListener() { if (!window.Echo || !window.chatContext) { @@ -708,6 +722,18 @@ e.expire_seconds, e.type || 'gold', ); + }) + .listen('.red-packet.claimed', (e) => { + if (Number(e.envelope_id) !== Number(_rpEnvelopeId)) { + return; + } + + window.updateRedPacketClaimsUI( + e.claimer_username, + e.amount, + e.remaining_count, + e.type || _rpType, + ); }); console.log('RedPacketSent 监听器已注册'); } diff --git a/tests/Feature/RedPacketControllerTest.php b/tests/Feature/RedPacketControllerTest.php index d7ee26e..43b77c0 100644 --- a/tests/Feature/RedPacketControllerTest.php +++ b/tests/Feature/RedPacketControllerTest.php @@ -2,17 +2,25 @@ namespace Tests\Feature; +use App\Events\RedPacketClaimed; use App\Models\RedPacketEnvelope; use App\Models\Sysparam; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Redis; use Tests\TestCase; +/** + * 类功能:验证礼包红包控制器的发包、领取与状态查询流程 + */ class RedPacketControllerTest extends TestCase { use RefreshDatabase; + /** + * 方法功能:初始化红包测试所需的 Redis 与站长等级配置。 + */ protected function setUp(): void { parent::setUp(); @@ -20,7 +28,10 @@ class RedPacketControllerTest extends TestCase Sysparam::updateOrCreate(['alias' => 'superlevel'], ['body' => '100']); } - public function test_normal_user_cannot_send_red_packet() + /** + * 方法功能:验证普通用户不能发送礼包。 + */ + public function test_normal_user_cannot_send_red_packet(): void { $user = User::factory()->create(['user_level' => 10]); @@ -33,7 +44,10 @@ class RedPacketControllerTest extends TestCase $response->assertJson(['status' => 'error']); } - public function test_superadmin_can_send_red_packet() + /** + * 方法功能:验证站长可以成功发出礼包并写入 Redis 拆包结果。 + */ + public function test_superadmin_can_send_red_packet(): void { $admin = User::factory()->create(['user_level' => 100]); @@ -57,7 +71,10 @@ class RedPacketControllerTest extends TestCase $this->assertEquals(10, Redis::llen("red_packet:{$envelope->id}:amounts")); } - public function test_cannot_send_multiple_active_packets_in_same_room() + /** + * 方法功能:验证同一房间内不可重复发送未结束的礼包。 + */ + public function test_cannot_send_multiple_active_packets_in_same_room(): void { $admin = User::factory()->create(['user_level' => 100]); @@ -74,7 +91,10 @@ class RedPacketControllerTest extends TestCase $response->assertStatus(422); } - public function test_user_can_claim_red_packet() + /** + * 方法功能:验证用户可以领取礼包并增加金币余额。 + */ + public function test_user_can_claim_red_packet(): void { $admin = User::factory()->create(['user_level' => 100]); $user = User::factory()->create(['jjb' => 100]); @@ -104,7 +124,46 @@ class RedPacketControllerTest extends TestCase $this->assertGreaterThan(100, $user->fresh()->jjb); } - public function test_user_cannot_claim_same_packet_twice() + /** + * 方法功能:验证领取成功后会返回并广播最新剩余份数。 + */ + public function test_claim_red_packet_returns_and_broadcasts_remaining_count(): void + { + Event::fake([RedPacketClaimed::class]); + + $admin = User::factory()->create(['user_level' => 100]); + $user = User::factory()->create(['jjb' => 100]); + + $this->actingAs($admin)->postJson(route('command.red_packet.send'), [ + 'room_id' => 1, + 'type' => 'gold', + ]); + + $envelope = RedPacketEnvelope::query()->firstOrFail(); + + $response = $this->actingAs($user)->postJson(route('red_packet.claim', ['envelopeId' => $envelope->id]), [ + 'room_id' => 1, + ]); + + $response->assertOk(); + $response->assertJson([ + 'status' => 'success', + 'remaining_count' => 9, + ]); + + Event::assertDispatched(RedPacketClaimed::class, function (RedPacketClaimed $event) use ($user, $envelope): bool { + return $event->claimer->is($user) + && $event->envelopeId === $envelope->id + && $event->roomId === 1 + && $event->remainingCount === 9 + && $event->type === 'gold'; + }); + } + + /** + * 方法功能:验证同一用户不能重复领取同一个礼包。 + */ + public function test_user_cannot_claim_same_packet_twice(): void { $admin = User::factory()->create(['user_level' => 100]); $user = User::factory()->create(); @@ -130,7 +189,10 @@ class RedPacketControllerTest extends TestCase $response->assertJson(['message' => '您已经领过这个礼包了']); } - public function test_can_check_packet_status() + /** + * 方法功能:验证可以查询礼包状态并识别当前用户是否已领取。 + */ + public function test_can_check_packet_status(): void { $admin = User::factory()->create(['user_level' => 100]); $user = User::factory()->create();