1.修复预约开奖BUG

This commit is contained in:
2026-05-26 16:34:59 +08:00
parent 53eefd901d
commit 0122a0301f

View File

@@ -535,6 +535,11 @@ final class GameLiveService
'pending_draw_number' => $manualNumber,
'update_time' => time(),
]);
$saved = Db::name('game_record')->where('id', $rid)->value('pending_draw_number');
$savedParsed = filter_var($saved, FILTER_VALIDATE_INT);
if ($savedParsed === false || $savedParsed !== $manualNumber) {
return ['ok' => false, 'msg' => __('Failed to save scheduled draw number; please try again')];
}
GameHotDataCoordinator::afterGameRecordCommitted($rid);
self::publishSnapshot(null);
} finally {
@@ -575,96 +580,105 @@ final class GameLiveService
return ['ok' => false, 'msg' => __('Another operation is in progress for this period; please try again later')];
}
try {
self::ensureAiLocked($rid);
$record = self::reloadRecord($rid);
if (!$record) {
return ['ok' => false, 'msg' => __('No active game in progress')];
}
self::ensureAiLocked($rid);
$useManual = $manualNumber;
if ($useManual === null) {
$p = $record['pending_draw_number'] ?? null;
if ($p !== null && $p !== '' && is_numeric((string) $p)) {
$pn = (int) $p;
if ($pn >= 1 && $pn <= self::DRAW_NUMBER_MAX) {
$useManual = $pn;
}
}
}
$finalNumber = null;
$drawMode = 0;
if ($useManual !== null && $useManual >= 1 && $useManual <= self::DRAW_NUMBER_MAX) {
$finalNumber = $useManual;
$drawMode = 1;
} else {
$al = $record['ai_locked_number'] ?? null;
if ($al !== null && $al !== '' && is_numeric((string) $al)) {
$finalNumber = (int) $al;
}
}
if ($finalNumber === null || $finalNumber < 1) {
$bets = Db::name('bet_order')->where('period_id', (int) $record['id'])->select()->toArray();
$finalNumber = self::computeBestNumberFromBets($bets) ?? 1;
$settleOut = ['jackpot_hits' => [], 'bet_wins' => []];
$finalNumber = 0;
$drawMode = 0;
}
$finalLoss = '0.00';
$payoutUntil = 0;
$periodNo = '';
$now = time();
$bets = Db::name('bet_order')->where('period_id', (int) $record['id'])->select()->toArray();
$finalLoss = self::estimateLossForNumber($bets, $finalNumber);
$now = time();
$payoutUntil = $now + self::getPayoutGraceSeconds();
Db::startTrans();
try {
$record = self::loadRecordRowFromDb($rid, true);
if (!$record) {
Db::rollback();
return ['ok' => false, 'msg' => __('No active game in progress')];
}
$settleOut = ['jackpot_hits' => [], 'bet_wins' => []];
Db::startTrans();
try {
Db::name('game_record')->where('id', (int) $record['id'])->update([
'status' => 3,
$st = (int) ($record['status'] ?? -1);
$existingResult = filter_var($record['result_number'] ?? 0, FILTER_VALIDATE_INT);
if ($existingResult !== false && $existingResult >= 1 && $existingResult <= self::DRAW_NUMBER_MAX && $st >= 2) {
Db::commit();
$periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : '';
return [
'ok' => true,
'msg' => __('Draw completed; paying out'),
'result_number' => $existingResult,
'estimated_loss' => '0.00',
'payout_until' => (int) ($record['payout_until'] ?? 0),
];
}
if (!in_array($st, [0, 1], true)) {
Db::rollback();
return ['ok' => false, 'msg' => __('Current game status does not allow drawing')];
}
$elapsedLocked = max(0, $now - (int) ($record['period_start_at'] ?? $now));
if ($elapsedLocked < $betSeconds || $elapsedLocked < $periodSeconds) {
Db::rollback();
return ['ok' => false, 'msg' => __('Period countdown has not ended; cannot draw yet')];
}
[$finalNumber, $drawMode] = self::resolveFinalDrawNumber($record, $manualNumber);
$bets = Db::name('bet_order')->where('period_id', $rid)->select()->toArray();
$finalLoss = self::estimateLossForNumber($bets, $finalNumber);
$payoutUntil = $now + self::getPayoutGraceSeconds();
$periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : '';
Db::name('game_record')->where('id', $rid)->update([
'status' => 3,
'result_number' => $finalNumber,
'draw_mode' => $drawMode,
'pending_draw_number' => null,
'payout_until' => $payoutUntil,
'update_time' => $now,
]);
$settleOut = GameBetSettleService::settleBetsForDraw($rid, $finalNumber);
Db::commit();
} catch (Throwable $e) {
Db::rollback();
return ['ok' => false, 'msg' => __('Game live: settlement error') . ': ' . $e->getMessage()];
}
GameBetSettleService::publishSettlementWinsAfterCommit(
$settleOut,
$rid,
$periodNo,
$finalNumber
);
GameHotDataCoordinator::afterGameRecordCommitted($rid);
try {
GameRecordStatService::refreshForRecordId($rid);
} catch (Throwable) {
}
self::publishPublicPeriodOpened($periodNo, $finalNumber, $drawMode, $now);
self::publishPublicPeriodPayout($rid, $periodNo, $finalNumber, $payoutUntil, $now);
$jackpotHits = is_array($settleOut['jackpot_hits'] ?? null) ? $settleOut['jackpot_hits'] : [];
GameWebSocketEventBus::publish('admin.live.opened', [
'period_id' => $rid,
'period_no' => $periodNo,
'result_number' => $finalNumber,
'draw_mode' => $drawMode,
'pending_draw_number' => null,
'payout_until' => $payoutUntil,
'update_time' => $now,
'jackpot_hits' => $jackpotHits,
'server_time' => $now,
]);
$settleOut = GameBetSettleService::settleBetsForDraw((int) $record['id'], $finalNumber);
Db::commit();
} catch (Throwable $e) {
Db::rollback();
return ['ok' => false, 'msg' => __('Game live: settlement error') . ': ' . $e->getMessage()];
}
self::publishSnapshot(null);
GameBetSettleService::publishSettlementWinsAfterCommit(
$settleOut,
$rid,
(string) $record['period_no'],
$finalNumber
);
GameHotDataCoordinator::afterGameRecordCommitted($rid);
try {
GameRecordStatService::refreshForRecordId($rid);
} catch (Throwable) {
}
self::publishPublicPeriodOpened((string) $record['period_no'], $finalNumber, $now);
self::publishPublicPeriodPayout($rid, (string) $record['period_no'], $finalNumber, $payoutUntil, $now);
$jackpotHits = is_array($settleOut['jackpot_hits'] ?? null) ? $settleOut['jackpot_hits'] : [];
GameWebSocketEventBus::publish('admin.live.opened', [
'period_id' => $rid,
'period_no' => (string) $record['period_no'],
'result_number' => $finalNumber,
'payout_until' => $payoutUntil,
'jackpot_hits' => $jackpotHits,
'server_time' => $now,
]);
self::publishSnapshot(null);
return [
'ok' => true,
'msg' => __('Draw completed; paying out'),
'result_number' => $finalNumber,
'estimated_loss' => $finalLoss,
'payout_until' => $payoutUntil,
];
return [
'ok' => true,
'msg' => __('Draw completed; paying out'),
'result_number' => $finalNumber,
'draw_mode' => $drawMode,
'estimated_loss' => $finalLoss,
'payout_until' => $payoutUntil,
];
} finally {
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, $lock['token'], $lock['redis_lock']);
}
@@ -734,19 +748,12 @@ final class GameLiveService
return;
}
}
$record = self::resolveRecord(null);
$record = self::resolveRecordForAutoDraw();
if (!$record || !in_array((int) $record['status'], [0, 1], true)) {
return;
}
$betSeconds = self::getConfigInt(self::KEY_BET_SECONDS, 20);
$periodSeconds = self::getConfigInt(self::KEY_PERIOD_SECONDS, 30);
$elapsed = max(0, time() - (int) $record['period_start_at']);
self::ensureAiLocked((int) $record['id']);
$record = self::reloadRecord((int) $record['id']);
if (!$record) {
return;
}
$elapsed = max(0, time() - (int) $record['period_start_at']);
if ($elapsed < $periodSeconds) {
return;
}
@@ -1219,11 +1226,12 @@ final class GameLiveService
]);
}
private static function publishPublicPeriodOpened(string $periodNo, int $resultNumber, int $openTime): void
private static function publishPublicPeriodOpened(string $periodNo, int $resultNumber, int $drawMode, int $openTime): void
{
GameWebSocketEventBus::publish(self::EVT_PERIOD_OPENED, [
'period_no' => $periodNo,
'result_number' => $resultNumber,
'draw_mode' => $drawMode,
'open_time' => $openTime,
]);
}
@@ -1314,6 +1322,84 @@ final class GameLiveService
return $row ?: null;
}
/**
* 自动开奖目标期:优先取已预约开奖号码的进行中局,避免开错期。
*
* @return array<string, mixed>|null
*/
private static function resolveRecordForAutoDraw(): ?array
{
$pendingRow = Db::name('game_record')
->whereIn('status', [0, 1])
->where('pending_draw_number', '>', 0)
->order('id', 'desc')
->find();
if (is_array($pendingRow)) {
return $pendingRow;
}
$row = Db::name('game_record')
->whereIn('status', [0, 1])
->order('id', 'desc')
->find();
return is_array($row) ? $row : null;
}
/**
* @return array<string, mixed>|null
*/
private static function loadRecordRowFromDb(int $recordId, bool $forUpdate = false): ?array
{
if ($recordId <= 0) {
return null;
}
$query = Db::name('game_record')->where('id', $recordId);
if ($forUpdate) {
$query->lock(true);
}
$row = $query->find();
return is_array($row) ? $row : null;
}
/**
* 开奖号码优先级:显式入参 > 预约开奖 pending_draw_number > AI 锁定号 > 按注单估算。
*
* @return array{0: int, 1: int} [finalNumber, drawMode] drawMode: 0=AI/估算 1=预约/手动
*/
private static function resolveFinalDrawNumber(array $record, ?int $manualOverride): array
{
$max = self::DRAW_NUMBER_MAX;
if ($manualOverride !== null && $manualOverride >= 1 && $manualOverride <= $max) {
return [$manualOverride, 1];
}
$pending = $record['pending_draw_number'] ?? null;
if ($pending !== null && $pending !== '' && is_numeric((string) $pending)) {
$pendingNumber = (int) $pending;
if ($pendingNumber >= 1 && $pendingNumber <= $max) {
return [$pendingNumber, 1];
}
}
$aiLocked = $record['ai_locked_number'] ?? null;
if ($aiLocked !== null && $aiLocked !== '' && is_numeric((string) $aiLocked)) {
$aiNumber = (int) $aiLocked;
if ($aiNumber >= 1 && $aiNumber <= $max) {
return [$aiNumber, 0];
}
}
$recordId = filter_var($record['id'] ?? 0, FILTER_VALIDATE_INT);
$bets = [];
if ($recordId !== false && $recordId > 0) {
$bets = Db::name('bet_order')->where('period_id', $recordId)->select()->toArray();
}
return [self::computeBestNumberFromBets($bets) ?? 1, 0];
}
/**
* 封盘后计算并锁定 AI 号码本期不变并封盘status 0→1
*/