feat: 彩票业务迁移并补全后台权限与代理结算体系

This commit is contained in:
2026-06-10 10:29:43 +08:00
parent bbdb69dabb
commit 1948b10fe6
108 changed files with 7083 additions and 5033 deletions

View File

@@ -17,6 +17,8 @@ use App\Models\DrawResultBatch;
use App\Models\SettlementBatch;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use App\Services\LotterySettings;
use App\Events\DrawCountdownBroadcast;
use App\Lottery\DrawResultBatchStatus;
@@ -38,6 +40,8 @@ beforeEach(function (): void {
'lottery.draw.require_manual_review' => false,
'lottery.draw.cooldown_minutes' => 15,
]);
Cache::flush();
});
test('draw planner fills buffer rows with ordered draw_no', function (): void {
@@ -1128,6 +1132,7 @@ test('lottery draw-tick command runs successfully', function (): void {
test('lottery hall-countdown dispatches draw.countdown when using reverb connection', function (): void {
Event::fake([DrawCountdownBroadcast::class]);
Cache::forget('lottery:hall:countdown:last-fingerprint');
config([
'broadcasting.default' => 'reverb',
'broadcasting.connections.reverb.driver' => 'reverb',
@@ -1156,6 +1161,56 @@ test('lottery hall-countdown dispatches draw.countdown when using reverb connect
);
});
test('lottery hall-countdown skips unchanged non-sync pulse but broadcasts on state change', function (): void {
Cache::forget('lottery:hall:countdown:last-fingerprint');
Event::fake([DrawCountdownBroadcast::class]);
config([
'broadcasting.default' => 'reverb',
'broadcasting.connections.reverb.driver' => 'reverb',
'lottery.realtime_hall_countdown_sync_interval_seconds' => 5,
]);
Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:00', 'UTC'));
$draw = Draw::query()->create([
'draw_no' => '20260509-002',
'business_date' => '2026-05-09',
'sequence_no' => 2,
'status' => DrawStatus::Open->value,
'start_time' => now()->subMinutes(5),
'close_time' => now()->addMinutes(1),
'draw_time' => now()->addMinutes(2),
'cooling_end_time' => null,
'result_source' => null,
'current_result_version' => 0,
'settle_version' => 0,
'is_reopened' => false,
]);
$this->artisan('lottery:hall-countdown')->assertSuccessful();
Event::assertDispatchedTimes(DrawCountdownBroadcast::class, 1);
Event::fake([DrawCountdownBroadcast::class]);
Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:01', 'UTC'));
$this->artisan('lottery:hall-countdown')->assertSuccessful();
Event::assertNotDispatched(DrawCountdownBroadcast::class);
Event::fake([DrawCountdownBroadcast::class]);
$draw->forceFill([
'status' => DrawStatus::Closing->value,
'close_time' => Carbon::parse('2026-05-09 12:00:00', 'UTC'),
])->save();
Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:02', 'UTC'));
$this->artisan('lottery:hall-countdown')->assertSuccessful();
Event::assertDispatched(
DrawCountdownBroadcast::class,
fn (DrawCountdownBroadcast $event): bool => ($event->data['status'] ?? null) === DrawStatus::Closing->value,
);
Carbon::setTestNow();
});
test('hall snapshot skips stale pending draw and picks next upcoming row', function (): void {
Carbon::setTestNow(Carbon::parse('2026-05-25 18:00:00', 'UTC'));
@@ -1243,6 +1298,80 @@ test('hall snapshot switches to next bettable draw when cooldown ended', functio
Carbon::setTestNow();
});
test('hall snapshot reuses cached heavy fragments between second-level countdown builds', function (): void {
Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:00', 'UTC'));
$draw = Draw::query()->create([
'draw_no' => '20260509-001',
'business_date' => '2026-05-09',
'sequence_no' => 1,
'status' => DrawStatus::Cooldown->value,
'start_time' => now()->subMinutes(5),
'close_time' => now()->subMinute(),
'draw_time' => now()->subSeconds(30),
'cooling_end_time' => now()->addMinutes(5),
'result_source' => 'rng',
'current_result_version' => 1,
'settle_version' => 0,
'is_reopened' => false,
]);
RiskPool::query()->create([
'draw_id' => $draw->id,
'normalized_number' => '1234',
'total_cap_amount' => 100_000,
'locked_amount' => 90_000,
'sold_out_status' => 0,
]);
$batch = DrawResultBatch::query()->create([
'draw_id' => $draw->id,
'result_version' => 1,
'source_type' => 'rng',
'rng_seed_hash' => hash('sha256', 'fixture'),
'raw_seed_encrypted' => null,
'status' => DrawResultBatchStatus::Published->value,
'created_by' => null,
'confirmed_by' => null,
'confirmed_at' => now(),
]);
DrawResultItem::query()->create([
'draw_id' => $draw->id,
'result_batch_id' => $batch->id,
'prize_type' => 'first',
'prize_index' => 0,
'number_4d' => '1234',
'suffix_3d' => '234',
'suffix_2d' => '34',
'head_digit' => 1,
'tail_digit' => 4,
]);
DB::enableQueryLog();
app(DrawHallSnapshotBuilder::class)->build(now()->utc());
$firstQueries = DB::getQueryLog();
DB::flushQueryLog();
Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:01', 'UTC'));
$payload = app(DrawHallSnapshotBuilder::class)->build(now()->utc());
$secondQueries = DB::getQueryLog();
DB::disableQueryLog();
$secondSql = collect($secondQueries)->pluck('query')->implode("\n");
expect($payload['draw_no'])->toBe('20260509-001')
->and($payload['result_items'][0]['number_4d'] ?? null)->toBe('1234')
->and($payload['risk_pool_alerts'][0]['normalized_number'] ?? null)->toBe('1234')
->and(count($secondQueries))->toBeLessThan(count($firstQueries))
->and($secondSql)->not->toContain('jackpot_pools')
->and($secondSql)->not->toContain('risk_pools')
->and($secondSql)->not->toContain('draw_result_batches')
->and($secondSql)->not->toContain('draw_result_items');
Carbon::setTestNow();
});
test('draw tick dispatches draw.status_change when hall draw_no or status changes', function (): void {
Event::fake([DrawStatusChangeBroadcast::class]);
config([