feat: 增强抽奖管理功能,支持手动创建、更新和删除期号
- 新增 API 路由和控制器,允许管理员手动创建、更新和删除抽奖期号。 - 更新抽奖调度逻辑,确保在抽奖时间和封盘时间的管理上更加灵活。 - 添加多语言支持的错误信息,提升用户体验。 - 更新测试用例,确保新功能的正确性和稳定性。
This commit is contained in:
@@ -72,6 +72,141 @@ test('admin can batch generate draw schedule buffer', function (): void {
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
test('draw planner schedules after last draw_time not midnight slot', function (): void {
|
||||
config([
|
||||
'lottery.draw.interval_minutes' => 5,
|
||||
'lottery.draw.buffer_draws_ahead' => 2,
|
||||
]);
|
||||
|
||||
$fixed = Carbon::parse('2026-05-25 11:00:00', 'UTC');
|
||||
$lastId = Draw::query()->create([
|
||||
'draw_no' => '20260525-120',
|
||||
'business_date' => '2026-05-25',
|
||||
'sequence_no' => 120,
|
||||
'status' => DrawStatus::Settled->value,
|
||||
'start_time' => Carbon::parse('2026-05-25 11:54:30', 'UTC'),
|
||||
'close_time' => Carbon::parse('2026-05-25 11:59:30', 'UTC'),
|
||||
'draw_time' => Carbon::parse('2026-05-25 12:00:00', 'UTC'),
|
||||
'cooling_end_time' => null,
|
||||
'result_source' => null,
|
||||
'current_result_version' => 1,
|
||||
'settle_version' => 1,
|
||||
'is_reopened' => false,
|
||||
])->id;
|
||||
|
||||
app(DrawPlannerService::class)->ensureBuffer($fixed);
|
||||
|
||||
$next = Draw::query()->where('id', '>', $lastId)->orderBy('draw_time')->first();
|
||||
expect($next)->not->toBeNull();
|
||||
expect($next->draw_time?->utc()->format('Y-m-d H:i:s'))->toBe('2026-05-25 12:05:00');
|
||||
expect($next->sequence_no)->toBe(121);
|
||||
});
|
||||
|
||||
test('admin can manually create draw with custom timeline', function (): void {
|
||||
Carbon::setTestNow(Carbon::parse('2026-05-25 08:00:00', 'UTC'));
|
||||
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'draw_create_admin',
|
||||
'name' => 'Draw Create Admin',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($admin);
|
||||
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson('/api/v1/admin/draws', [
|
||||
'draw_time' => '2026-05-25 12:00:00',
|
||||
'start_time' => '2026-05-25 11:55:00',
|
||||
'close_time' => '2026-05-25 11:59:30',
|
||||
'draw_no' => '20260525-901',
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.draw_no', '20260525-901')
|
||||
->assertJsonPath('data.status', DrawStatus::Pending->value);
|
||||
|
||||
$draw = Draw::query()->where('draw_no', '20260525-901')->first();
|
||||
expect($draw)->not->toBeNull();
|
||||
expect($draw->draw_time?->utc()->format('Y-m-d H:i:s'))->toBe('2026-05-25 12:00:00');
|
||||
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
test('admin can update pending draw timeline', function (): void {
|
||||
Carbon::setTestNow(Carbon::parse('2026-05-25 08:00:00', 'UTC'));
|
||||
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'draw_update_admin',
|
||||
'name' => 'Draw Update Admin',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($admin);
|
||||
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$draw = Draw::query()->create([
|
||||
'draw_no' => '20260525-902',
|
||||
'business_date' => '2026-05-25',
|
||||
'sequence_no' => 902,
|
||||
'status' => DrawStatus::Pending->value,
|
||||
'start_time' => Carbon::parse('2026-05-25 11:55:00', 'UTC'),
|
||||
'close_time' => Carbon::parse('2026-05-25 11:59:30', 'UTC'),
|
||||
'draw_time' => Carbon::parse('2026-05-25 12:00:00', 'UTC'),
|
||||
'cooling_end_time' => null,
|
||||
'result_source' => null,
|
||||
'current_result_version' => 0,
|
||||
'settle_version' => 0,
|
||||
'is_reopened' => false,
|
||||
]);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->putJson('/api/v1/admin/draws/'.$draw->id, [
|
||||
'draw_time' => '2026-05-25 13:00:00',
|
||||
'start_time' => '2026-05-25 12:55:00',
|
||||
'close_time' => '2026-05-25 12:59:30',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.draw_time', fn ($v) => str_contains((string) $v, '13:00'));
|
||||
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
test('admin can destroy pending draw without bets', function (): void {
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'draw_destroy_admin',
|
||||
'name' => 'Draw Destroy Admin',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($admin);
|
||||
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$draw = Draw::query()->create([
|
||||
'draw_no' => '20260525-903',
|
||||
'business_date' => '2026-05-25',
|
||||
'sequence_no' => 903,
|
||||
'status' => DrawStatus::Pending->value,
|
||||
'start_time' => now()->addHour(),
|
||||
'close_time' => now()->addHours(2),
|
||||
'draw_time' => now()->addHours(3),
|
||||
'cooling_end_time' => null,
|
||||
'result_source' => null,
|
||||
'current_result_version' => 0,
|
||||
'settle_version' => 0,
|
||||
'is_reopened' => false,
|
||||
]);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->deleteJson('/api/v1/admin/draws/'.$draw->id)
|
||||
->assertOk()
|
||||
->assertJsonPath('data.deleted', true);
|
||||
|
||||
expect(Draw::query()->find($draw->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('admin can manually close open draw', function (): void {
|
||||
Carbon::setTestNow(Carbon::parse('2026-05-09 12:10:00', 'UTC'));
|
||||
|
||||
@@ -617,6 +752,7 @@ test('GET draw current returns open draw with seconds to close', function (): vo
|
||||
|
||||
$this->getJson('/api/v1/draw/current')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.data.schedule_timezone', 'UTC')
|
||||
->assertJsonPath('data.data.draw_no', '20260509-300')
|
||||
->assertJsonPath('data.data.status', DrawStatus::Open->value)
|
||||
->assertJsonPath('data.data.seconds_to_close', 60 * 60 - 30)
|
||||
@@ -771,6 +907,50 @@ test('lottery hall-countdown dispatches draw.countdown when using reverb connect
|
||||
);
|
||||
});
|
||||
|
||||
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'));
|
||||
|
||||
Draw::query()->create([
|
||||
'draw_no' => '20260525-999',
|
||||
'business_date' => '2026-05-25',
|
||||
'sequence_no' => 999,
|
||||
'status' => DrawStatus::Pending->value,
|
||||
'start_time' => Carbon::parse('2026-05-25 17:32:00', 'UTC'),
|
||||
'close_time' => Carbon::parse('2026-05-25 17:36:30', 'UTC'),
|
||||
'draw_time' => Carbon::parse('2026-05-25 17:37:00', 'UTC'),
|
||||
'cooling_end_time' => null,
|
||||
'result_source' => null,
|
||||
'current_result_version' => 0,
|
||||
'settle_version' => 0,
|
||||
'is_reopened' => false,
|
||||
]);
|
||||
|
||||
Draw::query()->create([
|
||||
'draw_no' => '20260525-1006',
|
||||
'business_date' => '2026-05-25',
|
||||
'sequence_no' => 1006,
|
||||
'status' => DrawStatus::Pending->value,
|
||||
'start_time' => Carbon::parse('2026-05-25 18:07:00', 'UTC'),
|
||||
'close_time' => Carbon::parse('2026-05-25 18:11:30', 'UTC'),
|
||||
'draw_time' => Carbon::parse('2026-05-25 18:12:00', 'UTC'),
|
||||
'cooling_end_time' => null,
|
||||
'result_source' => null,
|
||||
'current_result_version' => 0,
|
||||
'settle_version' => 0,
|
||||
'is_reopened' => false,
|
||||
]);
|
||||
|
||||
$target = app(DrawHallSnapshotBuilder::class)->resolveHallTarget(now()->utc());
|
||||
expect($target)->not->toBeNull()
|
||||
->and($target->draw_no)->toBe('20260525-1006');
|
||||
|
||||
$payload = app(DrawHallSnapshotBuilder::class)->build(now()->utc());
|
||||
expect($payload['draw_no'])->toBe('20260525-1006')
|
||||
->and($payload['seconds_to_start'])->toBeGreaterThan(0);
|
||||
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
test('hall snapshot switches to next bettable draw when cooldown ended', function (): void {
|
||||
Carbon::setTestNow(Carbon::parse('2026-05-10 12:00:00', 'UTC'));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user