1.修复预约开奖BUG
This commit is contained in:
@@ -535,6 +535,11 @@ final class GameLiveService
|
|||||||
'pending_draw_number' => $manualNumber,
|
'pending_draw_number' => $manualNumber,
|
||||||
'update_time' => time(),
|
'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);
|
GameHotDataCoordinator::afterGameRecordCommitted($rid);
|
||||||
self::publishSnapshot(null);
|
self::publishSnapshot(null);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -575,96 +580,105 @@ final class GameLiveService
|
|||||||
return ['ok' => false, 'msg' => __('Another operation is in progress for this period; please try again later')];
|
return ['ok' => false, 'msg' => __('Another operation is in progress for this period; please try again later')];
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
self::ensureAiLocked($rid);
|
self::ensureAiLocked($rid);
|
||||||
$record = self::reloadRecord($rid);
|
|
||||||
if (!$record) {
|
|
||||||
return ['ok' => false, 'msg' => __('No active game in progress')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$useManual = $manualNumber;
|
$settleOut = ['jackpot_hits' => [], 'bet_wins' => []];
|
||||||
if ($useManual === null) {
|
$finalNumber = 0;
|
||||||
$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;
|
|
||||||
$drawMode = 0;
|
$drawMode = 0;
|
||||||
}
|
$finalLoss = '0.00';
|
||||||
|
$payoutUntil = 0;
|
||||||
|
$periodNo = '';
|
||||||
|
$now = time();
|
||||||
|
|
||||||
$bets = Db::name('bet_order')->where('period_id', (int) $record['id'])->select()->toArray();
|
Db::startTrans();
|
||||||
$finalLoss = self::estimateLossForNumber($bets, $finalNumber);
|
try {
|
||||||
$now = time();
|
$record = self::loadRecordRowFromDb($rid, true);
|
||||||
$payoutUntil = $now + self::getPayoutGraceSeconds();
|
if (!$record) {
|
||||||
|
Db::rollback();
|
||||||
|
return ['ok' => false, 'msg' => __('No active game in progress')];
|
||||||
|
}
|
||||||
|
|
||||||
$settleOut = ['jackpot_hits' => [], 'bet_wins' => []];
|
$st = (int) ($record['status'] ?? -1);
|
||||||
Db::startTrans();
|
$existingResult = filter_var($record['result_number'] ?? 0, FILTER_VALIDATE_INT);
|
||||||
try {
|
if ($existingResult !== false && $existingResult >= 1 && $existingResult <= self::DRAW_NUMBER_MAX && $st >= 2) {
|
||||||
Db::name('game_record')->where('id', (int) $record['id'])->update([
|
Db::commit();
|
||||||
'status' => 3,
|
$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,
|
'result_number' => $finalNumber,
|
||||||
'draw_mode' => $drawMode,
|
'draw_mode' => $drawMode,
|
||||||
'pending_draw_number' => null,
|
|
||||||
'payout_until' => $payoutUntil,
|
'payout_until' => $payoutUntil,
|
||||||
'update_time' => $now,
|
'jackpot_hits' => $jackpotHits,
|
||||||
|
'server_time' => $now,
|
||||||
]);
|
]);
|
||||||
$settleOut = GameBetSettleService::settleBetsForDraw((int) $record['id'], $finalNumber);
|
self::publishSnapshot(null);
|
||||||
Db::commit();
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
Db::rollback();
|
|
||||||
return ['ok' => false, 'msg' => __('Game live: settlement error') . ': ' . $e->getMessage()];
|
|
||||||
}
|
|
||||||
|
|
||||||
GameBetSettleService::publishSettlementWinsAfterCommit(
|
return [
|
||||||
$settleOut,
|
'ok' => true,
|
||||||
$rid,
|
'msg' => __('Draw completed; paying out'),
|
||||||
(string) $record['period_no'],
|
'result_number' => $finalNumber,
|
||||||
$finalNumber
|
'draw_mode' => $drawMode,
|
||||||
);
|
'estimated_loss' => $finalLoss,
|
||||||
|
'payout_until' => $payoutUntil,
|
||||||
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,
|
|
||||||
];
|
|
||||||
} finally {
|
} finally {
|
||||||
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, $lock['token'], $lock['redis_lock']);
|
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, $lock['token'], $lock['redis_lock']);
|
||||||
}
|
}
|
||||||
@@ -734,19 +748,12 @@ final class GameLiveService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$record = self::resolveRecord(null);
|
$record = self::resolveRecordForAutoDraw();
|
||||||
if (!$record || !in_array((int) $record['status'], [0, 1], true)) {
|
if (!$record || !in_array((int) $record['status'], [0, 1], true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$betSeconds = self::getConfigInt(self::KEY_BET_SECONDS, 20);
|
|
||||||
$periodSeconds = self::getConfigInt(self::KEY_PERIOD_SECONDS, 30);
|
$periodSeconds = self::getConfigInt(self::KEY_PERIOD_SECONDS, 30);
|
||||||
$elapsed = max(0, time() - (int) $record['period_start_at']);
|
$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) {
|
if ($elapsed < $periodSeconds) {
|
||||||
return;
|
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, [
|
GameWebSocketEventBus::publish(self::EVT_PERIOD_OPENED, [
|
||||||
'period_no' => $periodNo,
|
'period_no' => $periodNo,
|
||||||
'result_number' => $resultNumber,
|
'result_number' => $resultNumber,
|
||||||
|
'draw_mode' => $drawMode,
|
||||||
'open_time' => $openTime,
|
'open_time' => $openTime,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -1314,6 +1322,84 @@ final class GameLiveService
|
|||||||
return $row ?: null;
|
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)。
|
* 封盘后计算并锁定 AI 号码(本期不变),并封盘(status 0→1)。
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user