feat: 更新玩法配置管理,简化字段并增强功能
- 将玩法相关的显示名称字段统一为 `display_name`,移除多语言字段。 - 在 `PlayTypePatchController` 中新增即时切换玩法开关的功能,并推送大厅更新。 - 优化多个控制器和服务中的权限检查与数据处理逻辑,提升代码可读性与维护性。
This commit is contained in:
96
app/Console/Commands/LotteryPerfDrawScheduleAuditCommand.php
Normal file
96
app/Console/Commands/LotteryPerfDrawScheduleAuditCommand.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Draw;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\Draw\DrawPlannerService;
|
||||
|
||||
/**
|
||||
* PRD §17.2:校验期号计划相邻开奖时刻间隔是否符合 interval_minutes(默认 5 分钟)。
|
||||
*/
|
||||
final class LotteryPerfDrawScheduleAuditCommand extends Command
|
||||
{
|
||||
protected $signature = 'lottery:perf-draw-schedule-audit
|
||||
{--samples=48 : 抽检相邻期数}
|
||||
{--tolerance-seconds=60 : 允许偏差(秒)}';
|
||||
|
||||
protected $description = 'Audit draw_time spacing for schedule punctuality (§17.2)';
|
||||
|
||||
public function handle(DrawPlannerService $planner): int
|
||||
{
|
||||
$samples = max(2, (int) $this->option('samples'));
|
||||
$tolerance = max(0, (int) $this->option('tolerance-seconds'));
|
||||
$intervalMinutes = (int) config('lottery.draw.interval_minutes', 5);
|
||||
$expectedSeconds = $intervalMinutes * 60;
|
||||
|
||||
$planner->ensureBuffer(Carbon::now('UTC'));
|
||||
|
||||
$nowUtc = Carbon::now('UTC');
|
||||
$horizon = $nowUtc->copy()->addDays(14);
|
||||
|
||||
$businessDate = Draw::query()
|
||||
->whereNotNull('draw_time')
|
||||
->where('draw_time', '>', $nowUtc)
|
||||
->where('draw_time', '<=', $horizon)
|
||||
->where('business_date', '<', '2090-01-01')
|
||||
->orderByDesc('business_date')
|
||||
->value('business_date');
|
||||
|
||||
if ($businessDate === null) {
|
||||
$this->error('No upcoming draws found.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$times = Draw::query()
|
||||
->where('business_date', $businessDate)
|
||||
->whereNotNull('draw_time')
|
||||
->orderBy('sequence_no')
|
||||
->limit($samples + 1)
|
||||
->pluck('draw_time')
|
||||
->map(fn ($t) => Carbon::parse($t)->utc())
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$this->line('business_date='.$businessDate);
|
||||
|
||||
if (count($times) < 2) {
|
||||
$this->error('Not enough draws to audit.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$violations = [];
|
||||
for ($i = 1; $i < count($times); $i++) {
|
||||
$delta = $times[$i]->diffInSeconds($times[$i - 1], absolute: true);
|
||||
if (abs($delta - $expectedSeconds) > $tolerance) {
|
||||
$violations[] = [
|
||||
'pair' => ($i - 1).'→'.$i,
|
||||
'delta_seconds' => $delta,
|
||||
'expected_seconds' => $expectedSeconds,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->info(sprintf(
|
||||
'interval_minutes=%d expected_gap=%ds tolerance=±%ds pairs_checked=%d',
|
||||
$intervalMinutes,
|
||||
$expectedSeconds,
|
||||
$tolerance,
|
||||
count($times) - 1,
|
||||
));
|
||||
|
||||
if ($violations === []) {
|
||||
$this->info('PASS — all checked gaps within tolerance.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->error('FAIL — spacing violations:');
|
||||
$this->table(['pair', 'delta_seconds', 'expected_seconds'], $violations);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
243
app/Console/Commands/LotteryPerfSettlementBenchmarkCommand.php
Normal file
243
app/Console/Commands/LotteryPerfSettlementBenchmarkCommand.php
Normal file
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Draw;
|
||||
use App\Models\Player;
|
||||
use App\Models\TicketItem;
|
||||
use App\Models\TicketOrder;
|
||||
use App\Models\PlayerWallet;
|
||||
use App\Models\DrawResultItem;
|
||||
use App\Models\DrawResultBatch;
|
||||
use App\Lottery\DrawStatus;
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\Draw\DrawPrizeLayout;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Lottery\DrawResultBatchStatus;
|
||||
use App\Services\Settlement\SettlementOrchestrator;
|
||||
|
||||
/**
|
||||
* PRD §17.2:单期万级注单结算耗时验收(仅结算编排,不含派彩入账)。
|
||||
*/
|
||||
final class LotteryPerfSettlementBenchmarkCommand extends Command
|
||||
{
|
||||
protected $signature = 'lottery:perf-settlement-benchmark
|
||||
{--items=10000 : 待结算 ticket_items 数量}
|
||||
{--max-seconds=30 : 通过阈值(秒)}';
|
||||
|
||||
protected $description = 'Benchmark SettlementOrchestrator for N pending_draw items (§17.2)';
|
||||
|
||||
public function handle(SettlementOrchestrator $orchestrator): int
|
||||
{
|
||||
if (ini_get('memory_limit') !== '-1' && $this->byteMemoryLimit(ini_get('memory_limit')) < 512 * 1024 * 1024) {
|
||||
ini_set('memory_limit', '512M');
|
||||
}
|
||||
|
||||
return $this->runBenchmark($orchestrator);
|
||||
}
|
||||
|
||||
private function runBenchmark(SettlementOrchestrator $orchestrator): int
|
||||
{
|
||||
$itemCount = max(1, (int) $this->option('items'));
|
||||
$maxSeconds = max(1, (int) $this->option('max-seconds'));
|
||||
$maxMs = $maxSeconds * 1000;
|
||||
|
||||
$this->info(sprintf('Seeding %d ticket items…', $itemCount));
|
||||
|
||||
$fixture = $this->seedFixture($itemCount);
|
||||
$draw = $fixture['draw'];
|
||||
|
||||
$this->info('Running trySettleDraw…');
|
||||
$started = hrtime(true);
|
||||
$ran = $orchestrator->trySettleDraw($draw->fresh());
|
||||
$elapsedMs = (int) ((hrtime(true) - $started) / 1_000_000);
|
||||
|
||||
if (! $ran) {
|
||||
$this->error('Settlement did not run (check draw status / published result batch).');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$settled = TicketItem::query()
|
||||
->where('draw_id', $draw->id)
|
||||
->whereIn('status', ['pending_payout', 'settled_lose', 'settled_win'])
|
||||
->count();
|
||||
|
||||
$pass = $elapsedMs <= $maxMs && $settled === $itemCount;
|
||||
|
||||
$this->table(
|
||||
['metric', 'value'],
|
||||
[
|
||||
['items', (string) $itemCount],
|
||||
['settled_rows', (string) $settled],
|
||||
['elapsed_ms', (string) $elapsedMs],
|
||||
['threshold_ms', (string) $maxMs],
|
||||
['result', $pass ? 'PASS' : 'FAIL'],
|
||||
],
|
||||
);
|
||||
|
||||
return $pass ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
|
||||
private function byteMemoryLimit(string $value): int
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '' || $value === '-1') {
|
||||
return PHP_INT_MAX;
|
||||
}
|
||||
|
||||
$unit = strtolower(substr($value, -1));
|
||||
$number = (int) $value;
|
||||
|
||||
return match ($unit) {
|
||||
'g' => $number * 1024 * 1024 * 1024,
|
||||
'm' => $number * 1024 * 1024,
|
||||
'k' => $number * 1024,
|
||||
default => $number,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{draw: Draw}
|
||||
*/
|
||||
private function seedFixture(int $itemCount): array
|
||||
{
|
||||
return DB::transaction(function () use ($itemCount): array {
|
||||
$uniq = bin2hex(random_bytes(4));
|
||||
$player = Player::query()->create([
|
||||
'site_code' => 'perf',
|
||||
'site_player_id' => 'perf-settle-'.$uniq,
|
||||
'username' => 'perf_'.$uniq,
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
PlayerWallet::query()->create([
|
||||
'player_id' => $player->id,
|
||||
'wallet_type' => 'lottery',
|
||||
'currency_code' => 'NPR',
|
||||
'balance' => 0,
|
||||
'frozen_balance' => 0,
|
||||
'status' => 0,
|
||||
'version' => 0,
|
||||
]);
|
||||
|
||||
$draw = Draw::query()->create([
|
||||
'draw_no' => '20990101-'.str_pad((string) random_int(1, 999), 3, '0', STR_PAD_LEFT),
|
||||
'business_date' => '2099-01-01',
|
||||
'sequence_no' => 1,
|
||||
'status' => DrawStatus::Settling->value,
|
||||
'start_time' => now()->subHour(),
|
||||
'close_time' => now()->subMinutes(30),
|
||||
'draw_time' => now()->subMinutes(20),
|
||||
'cooling_end_time' => now()->subMinutes(5),
|
||||
'result_source' => 'rng',
|
||||
'current_result_version' => 1,
|
||||
'settle_version' => 0,
|
||||
'is_reopened' => false,
|
||||
]);
|
||||
|
||||
$batch = DrawResultBatch::query()->create([
|
||||
'draw_id' => $draw->id,
|
||||
'result_version' => 1,
|
||||
'source_type' => 'rng',
|
||||
'rng_seed_hash' => 'perf-bench',
|
||||
'raw_seed_encrypted' => null,
|
||||
'status' => DrawResultBatchStatus::Published->value,
|
||||
'created_by' => null,
|
||||
'confirmed_by' => null,
|
||||
'confirmed_at' => now(),
|
||||
]);
|
||||
|
||||
foreach (DrawPrizeLayout::slots() as $slot) {
|
||||
DrawResultItem::query()->create([
|
||||
'draw_id' => $draw->id,
|
||||
'result_batch_id' => $batch->id,
|
||||
'prize_type' => $slot['prize_type'],
|
||||
'prize_index' => $slot['prize_index'],
|
||||
'number_4d' => '0001',
|
||||
'suffix_3d' => '001',
|
||||
'suffix_2d' => '01',
|
||||
'head_digit' => 0,
|
||||
'tail_digit' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
$order = TicketOrder::query()->create([
|
||||
'order_no' => 'TO-PERF-'.$uniq,
|
||||
'player_id' => $player->id,
|
||||
'draw_id' => $draw->id,
|
||||
'currency_code' => 'NPR',
|
||||
'total_bet_amount' => $itemCount * 10,
|
||||
'total_rebate_amount' => 0,
|
||||
'total_actual_deduct' => $itemCount * 10,
|
||||
'total_estimated_payout' => 0,
|
||||
'status' => 'placed',
|
||||
'submit_source' => 'perf',
|
||||
'client_trace_id' => 'perf-settle-'.$uniq,
|
||||
]);
|
||||
|
||||
$now = now();
|
||||
$oddsJson = json_encode(['1st' => 250000], JSON_THROW_ON_ERROR);
|
||||
$ruleJson = json_encode([], JSON_THROW_ON_ERROR);
|
||||
$chunk = 500;
|
||||
|
||||
for ($offset = 0; $offset < $itemCount; $offset += $chunk) {
|
||||
$size = min($chunk, $itemCount - $offset);
|
||||
$itemRows = [];
|
||||
$ticketNos = [];
|
||||
|
||||
for ($i = 0; $i < $size; $i++) {
|
||||
$seq = $offset + $i + 1;
|
||||
$num = str_pad((string) ($seq % 10000), 4, '0', STR_PAD_LEFT);
|
||||
$ticketNo = sprintf('TK-PERF-%s-%06d', $uniq, $seq);
|
||||
$ticketNos[] = $ticketNo;
|
||||
$itemRows[] = [
|
||||
'ticket_no' => $ticketNo,
|
||||
'order_id' => $order->id,
|
||||
'player_id' => $player->id,
|
||||
'draw_id' => $draw->id,
|
||||
'original_number' => $num,
|
||||
'normalized_number' => $num,
|
||||
'play_code' => 'big',
|
||||
'dimension' => 4,
|
||||
'digit_slot' => null,
|
||||
'bet_mode' => 'straight',
|
||||
'unit_bet_amount' => 10,
|
||||
'total_bet_amount' => 10,
|
||||
'rebate_rate_snapshot' => 0,
|
||||
'commission_rate_snapshot' => 0,
|
||||
'actual_deduct_amount' => 10,
|
||||
'odds_snapshot_json' => $oddsJson,
|
||||
'rule_snapshot_json' => $ruleJson,
|
||||
'combination_count' => 1,
|
||||
'estimated_max_payout' => 250,
|
||||
'risk_locked_amount' => 0,
|
||||
'status' => 'pending_draw',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
DB::table('ticket_items')->insert($itemRows);
|
||||
|
||||
$comboRows = DB::table('ticket_items')
|
||||
->whereIn('ticket_no', $ticketNos)
|
||||
->get(['id', 'normalized_number'])
|
||||
->map(fn ($row): array => [
|
||||
'ticket_item_id' => $row->id,
|
||||
'combination_no' => 0,
|
||||
'number_4d' => $row->normalized_number,
|
||||
'bet_amount' => 10,
|
||||
'estimated_payout' => 250,
|
||||
'created_at' => $now,
|
||||
])
|
||||
->all();
|
||||
|
||||
DB::table('ticket_combinations')->insert($comboRows);
|
||||
}
|
||||
|
||||
return ['draw' => $draw];
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user