feat(draw): 支持多玩法provider及本地化结算时区功能
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled

- Draw相关控制器添加provider_code和provider_name字段支持
- 允许手动录入开奖批次时指定provider_code
- RNG开奖批次生成支持多provider,分别生成对应批次数据
- DrawPublishService和DrawManualResultService中支持基于provider_code管理开奖版本和发布流程
- DrawResultViewService调整,支持按provider汇总及筛选开奖结果
- Settlement处理逻辑调整,按provider区分待结算票据及批次,分开结算
- 结算期间管理相关服务支持使用站点本地结算时区计算开账建议和日期处理
- 增加AdminSite的settlement_timezone字段及相关请求验证和数据存取支持
- 优化AdminSettlementPeriod相关代码,防止存在未结清票据时关账
- Ticket明细返回接口添加结算时区信息,提升用户时间体验
- 多处查询排序和版本号处理改进,保证多provider数据正确顺序与一致性
This commit is contained in:
2026-07-09 15:48:21 +08:00
parent c6c10f1397
commit d4779660c8
38 changed files with 1121 additions and 271 deletions

View File

@@ -2,10 +2,12 @@
use App\Models\Draw;
use App\Models\Player;
use App\Models\BetProvider;
use App\Models\WalletTxn;
use App\Models\TicketItem;
use App\Lottery\DrawStatus;
use App\Models\TicketOrder;
use App\Models\TicketCombination;
use App\Models\PlayerWallet;
use App\Models\DrawResultItem;
use App\Models\DrawResultBatch;
@@ -145,6 +147,147 @@ test('settlement pays big winner and marks ticket settled', function (): void {
expect(WalletTxn::query()->where('biz_type', 'settle_payout')->count())->toBe(1);
});
test('settlement matches ticket items against their provider result batch', function (): void {
BetProvider::query()->updateOrCreate(['code' => 'SG'], ['name' => 'Singapore', 'is_enabled' => true, 'sort_order' => 10]);
BetProvider::query()->create(['code' => 'MY', 'name' => 'Malaysia', 'is_enabled' => true, 'sort_order' => 20]);
$player = Player::query()->create([
'site_code' => 'test',
'site_player_id' => 'provider-settle-p',
'username' => 'provider_settle_p',
'nickname' => null,
'default_currency' => 'NPR',
'status' => 0,
]);
$draw = Draw::query()->create([
'draw_no' => '20260511-901',
'business_date' => '2026-05-11',
'sequence_no' => 901,
'status' => DrawStatus::Settling->value,
'start_time' => now()->subMinutes(10),
'close_time' => now()->subMinutes(2),
'draw_time' => now()->subMinute(),
'cooling_end_time' => null,
'result_source' => 'manual',
'current_result_version' => 1,
'settle_version' => 0,
'is_reopened' => false,
]);
$order = TicketOrder::query()->create([
'order_no' => 'provider-settle-order',
'player_id' => $player->id,
'draw_id' => $draw->id,
'currency_code' => 'NPR',
'total_bet_amount' => 20_000,
'total_rebate_amount' => 0,
'total_actual_deduct' => 20_000,
'total_estimated_payout' => 200_000,
'status' => 'placed',
'submit_source' => 'test',
'client_trace_id' => 'provider-settle-trace',
]);
$snapshot = [['prize_scope' => 'first', 'odds_value' => 100_000]];
$sgItem = TicketItem::query()->create([
'ticket_no' => 'PROVIDER-SG-1',
'order_id' => $order->id,
'player_id' => $player->id,
'draw_id' => $draw->id,
'provider_code' => 'SG',
'provider_name' => 'Singapore',
'original_number' => '1234',
'normalized_number' => '1234',
'play_code' => 'pos_4a',
'bet_mode' => 'unit',
'unit_bet_amount' => 10_000,
'total_bet_amount' => 10_000,
'actual_deduct_amount' => 10_000,
'odds_snapshot_json' => $snapshot,
'rule_snapshot_json' => [],
'combination_count' => 1,
'estimated_max_payout' => 100_000,
'risk_locked_amount' => 0,
'status' => 'pending_draw',
'win_amount' => 0,
'jackpot_win_amount' => 0,
]);
$myItem = TicketItem::query()->create([
'ticket_no' => 'PROVIDER-MY-1',
'order_id' => $order->id,
'player_id' => $player->id,
'draw_id' => $draw->id,
'provider_code' => 'MY',
'provider_name' => 'Malaysia',
'original_number' => '1234',
'normalized_number' => '1234',
'play_code' => 'pos_4a',
'bet_mode' => 'unit',
'unit_bet_amount' => 10_000,
'total_bet_amount' => 10_000,
'actual_deduct_amount' => 10_000,
'odds_snapshot_json' => $snapshot,
'rule_snapshot_json' => [],
'combination_count' => 1,
'estimated_max_payout' => 100_000,
'risk_locked_amount' => 0,
'status' => 'pending_draw',
'win_amount' => 0,
'jackpot_win_amount' => 0,
]);
foreach ([$sgItem, $myItem] as $item) {
TicketCombination::query()->create([
'ticket_item_id' => $item->id,
'combination_no' => 1,
'number_4d' => '1234',
'bet_amount' => 10_000,
'estimated_payout' => 100_000,
'created_at' => now(),
]);
}
foreach ([['SG', '9999'], ['MY', '1234']] as [$providerCode, $firstNumber]) {
$batch = DrawResultBatch::query()->create([
'draw_id' => $draw->id,
'provider_code' => $providerCode,
'provider_name' => $providerCode === 'MY' ? 'Malaysia' : 'Singapore',
'result_version' => 1,
'source_type' => 'manual',
'rng_seed_hash' => null,
'raw_seed_encrypted' => null,
'status' => DrawResultBatchStatus::Published->value,
'created_by' => null,
'confirmed_by' => null,
'confirmed_at' => now(),
]);
foreach (DrawPrizeLayout::slots() as $slot) {
$num = $slot['prize_type'] === 'first' ? $firstNumber : '5678';
DrawResultItem::query()->create([
'draw_id' => $draw->id,
'result_batch_id' => $batch->id,
'prize_type' => $slot['prize_type'],
'prize_index' => $slot['prize_index'],
'number_4d' => $num,
'suffix_3d' => substr($num, -3),
'suffix_2d' => substr($num, -2),
'head_digit' => (int) substr($num, 0, 1),
'tail_digit' => (int) substr($num, 3, 1),
]);
}
}
expect(app(SettlementOrchestrator::class)->trySettleDraw($draw->fresh()))->toBeTrue();
expect($sgItem->fresh()->status)->toBe('settled_lose');
expect((int) $sgItem->fresh()->win_amount)->toBe(0);
expect($myItem->fresh()->status)->toBe('pending_payout');
expect((int) $myItem->fresh()->win_amount)->toBe(100_000);
expect(SettlementBatch::query()->where('draw_id', $draw->id)->count())->toBe(2);
});
test('admin settlement requires review before payout and can export report', function (): void {
$uniq = bin2hex(random_bytes(4));
$player = Player::query()->create([