1.修复预约开奖BUG
This commit is contained in:
@@ -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)。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user